Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I create an undo transaction in Word or Excel? (VSTO)

I notice that Project 2007 has the functions that allow operations that can be undone to be placed in a single stack item, or "undo transaction". For example:

Application.OpenUndoTransaction "Create 6 tasks"
Dim i As Integer
For i = 1 To 6
    ActiveProject.Tasks.Add "UndoMe " & i
Next
Application.CloseUndoTransaction 

What this means is that the user can undo all of the actions in a single undo action, rather than 6 times.

This would be great to implement in Word and/or Excel, as I'm doing some things in VSTO that make multiple changes at once, and it'll be a bit annoying for the user if they have to click on Undo several times if they make a mistake. Although those specific functions don't appear to exist, does anyone know if / how this can be done in some way?

like image 333
Gavin Avatar asked Mar 19 '09 06:03

Gavin


1 Answers

You can simulate transactional behavior in Word by overwriting the Undo and Redo command routines in VBA (I don't think that overwriting built-in Word commands is possible using VSTO alone, though). The start of a transaction is marked by adding a bookmark, the end is marked by removing the bookmark.

When calling undo, we check whether the transaction mark bookmark is present and repeat the undo until the marker is gone. Redo is working the same way. This mechanism supports transactional undo/redo of all modifications done to the document content. However, to allow undo/redo of modifications to the document properties a special mechanism needs to be implemented using the SetCustomProp macro. Document properties should not be set directly but via this macro only.

Update: I forgot to clearly mention that this approach only works with the keyboard shortcuts and the menu commands, clicking the toolbar button still does a single-step undo. We therefore decided to replace the toolbar buttons with custom ones. The code has been in use for quite a while With Word 2003 (it's not tested with Word 2007, so be prepared for surprise ;)

Option Explicit

' string constants for Undo mechanism
Public Const BM_IN_MACRO As String = "_InMacro_"

Public Const BM_DOC_PROP_CHANGE As String = "_DocPropChange_"
Public Const BM_DOC_PROP_NAME As String = "_DocPropName_"
Public Const BM_DOC_PROP_OLD_VALUE As String = "_DocPropOldValue_"
Public Const BM_DOC_PROP_NEW_VALUE As String = "_DocPropNewValue_"

'-----------------------------------------------------------------------------------
' Procedure : EditUndo
' Purpose   : Atomic undo of macros
'             Note: This macro only catches the menu command and the keyboard shortcut,
'                   not the toolbar command
'-----------------------------------------------------------------------------------
Public Sub EditUndo() ' Catches Ctrl-Z

    'On Error Resume Next
    Dim bRefresh As Boolean
    bRefresh = Application.ScreenUpdating
    Application.ScreenUpdating = False

    Do
        If ActiveDocument.Bookmarks.Exists(BM_DOC_PROP_CHANGE) Then
            Dim strPropName As String
            Dim strOldValue As String

            strPropName = ActiveDocument.Bookmarks(BM_DOC_PROP_NAME).Range.Text
            strOldValue = ActiveDocument.Bookmarks(BM_DOC_PROP_OLD_VALUE).Range.Text
            ActiveDocument.CustomDocumentProperties(strPropName).Value = strOldValue
        End If

    Loop While (ActiveDocument.Undo = True) _
       And ActiveDocument.Bookmarks.Exists(BM_IN_MACRO)

    Application.ScreenUpdating = bRefresh
End Sub

'-----------------------------------------------------------------------------------
' Procedure : EditRedo
' Purpose   : Atomic redo of macros
'             Note: This macro only catches the menu command and the keyboard shortcut,
'                   not the toolbar command
'-----------------------------------------------------------------------------------
Public Sub EditRedo() ' Catches Ctrl-Y

    Dim bRefresh As Boolean
    bRefresh = Application.ScreenUpdating
    Application.ScreenUpdating = False

    Do
        If ActiveDocument.Bookmarks.Exists(BM_DOC_PROP_CHANGE) Then
            Dim strPropName As String
            Dim strNewValue As String

            strPropName = ActiveDocument.Bookmarks(BM_DOC_PROP_NAME).Range.Text
            strNewValue = ActiveDocument.Bookmarks(BM_DOC_PROP_NEW_VALUE).Range.Text
            ActiveDocument.CustomDocumentProperties(strPropName).Value = strNewValue
        End If

    Loop While (ActiveDocument.Redo = True) _
       And ActiveDocument.Bookmarks.Exists(BM_IN_MACRO)

    Application.ScreenUpdating = bRefresh

End Sub

'-----------------------------------------------------------------------------------
' Procedure : SetCustomProp
' Purpose   : Sets a custom document property
'-----------------------------------------------------------------------------------
Public Function SetCustomProp(oDoc As Document, strName As String, strValue As String)

    Dim strOldValue As String

    On Error GoTo existsAlready
    strOldValue = ""
    oDoc.CustomDocumentProperties.Add _
        Name:=strName, LinkToContent:=False, Value:=Trim(strValue), _
        Type:=msoPropertyTypeString
    GoTo exitHere

existsAlready:
    strOldValue = oDoc.CustomDocumentProperties(strName).Value
    oDoc.CustomDocumentProperties(strName).Value = strValue

exitHere:
    ' support undo / redo of changes to the document properties
    'On Error Resume Next
    Dim bCalledWithoutUndoSupport  As Boolean

    If Not ActiveDocument.Bookmarks.Exists(BM_IN_MACRO) Then
        ActiveDocument.Range.Bookmarks.Add BM_IN_MACRO, ActiveDocument.Range
        bCalledWithoutUndoSupport = True
    End If

    Dim oRange As Range
    Set oRange = ActiveDocument.Range

    oRange.Collapse wdCollapseEnd
    oRange.Text = " "
    oRange.Bookmarks.Add "DocPropDummy_", oRange

    oRange.Collapse wdCollapseEnd
    oRange.Text = strName
    oRange.Bookmarks.Add BM_DOC_PROP_NAME, oRange

    oRange.Collapse wdCollapseEnd
    oRange.Text = strOldValue
    oRange.Bookmarks.Add BM_DOC_PROP_OLD_VALUE, oRange

    oRange.Collapse wdCollapseEnd
    oRange.Text = strValue
    oRange.Bookmarks.Add BM_DOC_PROP_NEW_VALUE, oRange

    oRange.Bookmarks.Add BM_DOC_PROP_CHANGE
    ActiveDocument.Bookmarks(BM_DOC_PROP_CHANGE).Delete

    Set oRange = ActiveDocument.Bookmarks(BM_DOC_PROP_NEW_VALUE).Range
    ActiveDocument.Bookmarks(BM_DOC_PROP_NEW_VALUE).Delete
    If Len(oRange.Text) > 0 Then oRange.Delete

    Set oRange = ActiveDocument.Bookmarks(BM_DOC_PROP_OLD_VALUE).Range
    ActiveDocument.Bookmarks(BM_DOC_PROP_OLD_VALUE).Delete
    If Len(oRange.Text) > 0 Then oRange.Delete

    Set oRange = ActiveDocument.Bookmarks(BM_DOC_PROP_NAME).Range
    ActiveDocument.Bookmarks(BM_DOC_PROP_NAME).Delete
    If Len(oRange.Text) > 0 Then oRange.Delete

    Set oRange = ActiveDocument.Bookmarks("DocPropDummy_").Range
    ActiveDocument.Bookmarks("DocPropDummy_").Delete
    If Len(oRange.Text) > 0 Then oRange.Delete

    If bCalledWithoutUndoSupport And ActiveDocument.Bookmarks.Exists(BM_IN_MACRO) Then
        ActiveDocument.Bookmarks(BM_IN_MACRO).Delete
    End If

End Function

'-----------------------------------------------------------------------------------
' Procedure : SampleUsage
' Purpose   : Demonstrates a transaction
'-----------------------------------------------------------------------------------
Private Sub SampleUsage()

    On Error Resume Next

    ' mark begin of transaction
    ActiveDocument.Range.Bookmarks.Add BM_IN_MACRO

    Selection.Text = "Hello World"
    ' do other stuff

    ' mark end of transaction
    ActiveDocument.Bookmarks(BM_IN_MACRO).Delete

End Sub
like image 64
Dirk Vollmar Avatar answered Oct 13 '22 21:10

Dirk Vollmar