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

后端 未结 4 1294
不思量自难忘°
不思量自难忘° 2020-12-09 13:42

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:



        
相关标签:
4条回答
  • 2020-12-09 13:52

    Word 2010 provides the ability to do this via the Application.UndoRecord object. See http://msdn.microsoft.com/en-us/library/hh128816.aspx

    0 讨论(0)
  • 2020-12-09 14:09

    I've been chewing on this one for a while. Here's my attempt at using a hidden document, then grabbing the WordOpenXML from the hidden document and placing it in the real document when needed to make any amount of VSTO actions a single undo.

    //Usage from ThisDocument VSTO Document level project
    public partial class ThisDocument
    {   
        //Used to buffer writing text & formatting to document (to save undo stack)
        public static DocBuffer buffer;
    
        //Attached Template
        public static Word.Template template;
    
        private void ThisDocument_Startup(object sender, System.EventArgs e)
        {           
            //Ignore changes to template (removes prompt to save changes to template)
            template = (Word.Template)this.Application.ActiveDocument.get_AttachedTemplate();
            template.Saved = true;            
    
            //Document buffer
            buffer = new DocBuffer();
    
            //Start buffer
            ThisDocument.buffer.Start();
    
            //This becomes one "undo"
            Word.Selection curSel = Globals.ThisDocument.Application.Selection;
            curSel.TypeText(" ");
            curSel.TypeBackspace();
            curSel.Font.Bold = 1;
            curSel.TypeText("Hello, world!");
            curSel.Font.Bold = 0;
            curSel.TypeText(" ");
    
            //end buffer, print out text
            ThisDocument.buffer.End();
        }
    
        void Application_DocumentBeforeClose(Microsoft.Office.Interop.Word.Document Doc, ref bool Cancel)
        {
            buffer.Close();
        }
    
        private void ThisDocument_Shutdown(object sender, System.EventArgs e)
        {
            buffer.Close();         
        }
    }
    

    Here is the DocBuffer Class:

    public class DocBuffer
    {
        //Word API Objects
        Word._Document HiddenDoc;
        Word.Selection curSel;
        Word.Template template;
    
        //ref parameters
        object missing = System.Type.Missing;
        object FalseObj = false; //flip this for docbuffer troubleshooting
        object templateObj;
    
        //Is docbuffer running?
        public Boolean started{ get; private set; }
    
        //Open document on new object
        public DocBuffer()
        {
            //Clear out unused buffer bookmarks
            Word.Bookmarks bookmarks = Globals.ThisDocument.Application.ActiveDocument.Bookmarks;
            bookmarks.ShowHidden = true;
    
            foreach (Word.Bookmark mark in bookmarks)
            {
                if (mark.Name.Contains("_buf"))
                {
                    mark.Delete();
                }
            }
    
            //Remove trail of undo's for clearing out the bookmarks
            Globals.ThisDocument.UndoClear();
    
            //Set up template
            template = ThisDocument.template;
            templateObj = template;
    
            //Open Blank document, then attach styles *and update
            HiddenDoc = Globals.ThisDocument.Application.Documents.Add(ref missing, ref missing, ref missing, ref FalseObj);
            HiddenDoc.set_AttachedTemplate(ref templateObj);
            HiddenDoc.UpdateStyles();
    
            //Tell hidden document it has been saved to remove rare prompt to save document
            HiddenDoc.Saved = true;
    
            //Make primary document active
            Globals.ThisDocument.Activate();
    
        }
    
        ~DocBuffer()
        {
            try
            {
                HiddenDoc.Close(ref FalseObj, ref missing, ref missing);
            }
            catch { }
        }
    
        public void Close()
        {
            try
            {
                HiddenDoc.Close(ref FalseObj, ref missing, ref missing);
            }
            catch { }
        }
    
        public void Start()
        {
            try
            {
                //Make hidden document active to receive selection
                HiddenDoc.Activate(); //results in a slight application focus loss
            }
            catch (System.Runtime.InteropServices.COMException ex)
            {
                if (ex.Message == "Object has been deleted.")
                {
                    //Open Blank document, then attach styles *and update
                    HiddenDoc = Globals.ThisDocument.Application.Documents.Add(ref missing, ref missing, ref missing, ref FalseObj);
                    HiddenDoc.set_AttachedTemplate(ref templateObj);
                    HiddenDoc.UpdateStyles();
                    HiddenDoc.Activate();
                }
                else
                    throw;
            }
    
            //Remove Continue Bookmark, if exists
            Word.Bookmarks hiddenDocBookmarks = Globals.ThisDocument.Application.ActiveDocument.Bookmarks;
            if (hiddenDocBookmarks.Exists("Continue"))
            {
                object deleteMarkObj = "Continue";
                Word.Bookmark deleteMark = hiddenDocBookmarks.get_Item(ref deleteMarkObj);
                deleteMark.Select();
                deleteMark.Delete();
            }
    
            //Tell hidden document it has been saved to remove rare prompt to save document
            HiddenDoc.Saved = true;
    
            //Keep track when started
            started = true;
        }
    
        //Used for non-modal dialogs to bring active document back up between text insertion
        public void Continue()
        {
            //Exit quietly if buffer hasn't started
            if (!started) return;
    
            //Verify hidden document is active
            if ((HiddenDoc as Word.Document) != Globals.ThisDocument.Application.ActiveDocument)
            {
                HiddenDoc.Activate();
            }
    
            //Hidden doc selection
            curSel = Globals.ThisDocument.Application.Selection;
    
            //Hidden doc range
            Word.Range bufDocRange;
    
            //Select entire doc, save range
            curSel.WholeStory();
            bufDocRange = curSel.Range;
    
            //Find end, put a bookmark there
            bufDocRange.SetRange(curSel.End, curSel.End);
            object bookmarkObj = bufDocRange;
    
            //Generate "Continue" hidden bookmark
            Word.Bookmark mark = Globals.ThisDocument.Application.ActiveDocument.Bookmarks.Add("Continue", ref bookmarkObj);
            mark.Select();
    
            //Tell hidden document it has been saved to remove rare prompt to save document
            HiddenDoc.Saved = true;
    
            //Make primary document active
            Globals.ThisDocument.Activate();
        }
    
        public void End()
        {
            //Exit quietly if buffer hasn't started
            if (!started) return;
    
            //Turn off buffer started flag
            started = false;
    
            //Verify hidden document is active
            if ((HiddenDoc as Word.Document) != Globals.ThisDocument.Application.ActiveDocument)
            {
                HiddenDoc.Activate();
            }
    
            //Remove Continue Bookmark, if exists
            Word.Bookmarks hiddenDocBookmarks = Globals.ThisDocument.Application.ActiveDocument.Bookmarks;
            hiddenDocBookmarks.ShowHidden = true;
            if (hiddenDocBookmarks.Exists("Continue"))
            {
                object deleteMarkObj = "Continue";
                Word.Bookmark deleteMark = hiddenDocBookmarks.get_Item(ref deleteMarkObj);
                deleteMark.Delete();
            }
    
            //Hidden doc selection
            curSel = Globals.ThisDocument.Application.Selection;
    
            //Hidden doc range
            Word.Range hiddenDocRange;
            Word.Range bufDocRange;
    
            //Select entire doc, save range
            curSel.WholeStory();
            bufDocRange = curSel.Range;
    
            //If cursor bookmark placed in, move there, else find end of text, put a bookmark there
            Boolean cursorFound = false;
            if (hiddenDocBookmarks.Exists("_cursor"))
            {
                object cursorBookmarkObj = "_cursor";
                Word.Bookmark cursorBookmark = hiddenDocBookmarks.get_Item(ref cursorBookmarkObj);
                bufDocRange.SetRange(cursorBookmark.Range.End, cursorBookmark.Range.End);
                cursorBookmark.Delete();
                cursorFound = true;
            }
            else
            {
                //The -2 is done because [range object].WordOpenXML likes to drop bookmarks at the end of the range
                bufDocRange.SetRange(curSel.End - 2, curSel.End - 2);
            }
    
            object bookmarkObj = bufDocRange;
    
            //Generate GUID for hidden bookmark
            System.Guid guid = System.Guid.NewGuid();
            String id = "_buf" + guid.ToString().Replace("-", string.Empty);
            Word.Bookmark mark = Globals.ThisDocument.Application.ActiveDocument.Bookmarks.Add(id, ref bookmarkObj);
    
            //Get OpenXML Text (Text with formatting)
            curSel.WholeStory();
            hiddenDocRange = curSel.Range;
            string XMLText = hiddenDocRange.WordOpenXML;
    
            //Clear out contents of buffer
            hiddenDocRange.Delete(ref missing, ref missing); //comment this for docbuffer troubleshooting
    
            //Tell hidden document it has been saved to remove rare prompt to save document
            HiddenDoc.Saved = true;
    
            //Make primary document active
            Globals.ThisDocument.Activate();
    
            //Get selection from new active document
            curSel = Globals.ThisDocument.Application.Selection;
    
            //insert buffered formatted text into main document
            curSel.InsertXML(XMLText, ref missing);
    
            //Place cursor at bookmark+1 (this is done due to WordOpenXML ignoring bookmarks at the end of the selection)
            Word.Bookmarks bookmarks = Globals.ThisDocument.Application.ActiveDocument.Bookmarks;
            bookmarks.ShowHidden = true;
    
            object stringObj = id;
            Word.Bookmark get_mark = bookmarks.get_Item(ref stringObj);
            bufDocRange = get_mark.Range;
    
            if (cursorFound) //Canned language actively placed cursor
                bufDocRange.SetRange(get_mark.Range.End, get_mark.Range.End);
            else //default cursor at the end of text
                bufDocRange.SetRange(get_mark.Range.End + 1, get_mark.Range.End + 1);
            bufDocRange.Select();
    }
    
    0 讨论(0)
  • 2020-12-09 14:13

    Excel has some (limited) built-in support for undo and redo as part of its VBA architecture.

    I'm not familiar with vsto, so I don't know if this will help you out, but you can take a look at this SO question for more details.

    0 讨论(0)
  • 2020-12-09 14:14

    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
    
    0 讨论(0)
提交回复
热议问题