[UPDATED] Word After Save Event


When I wrote my first Word AfterSave Event entry, it was designed for Word 2007, and was – as it turns out – not a catch all. So I have updated it here (thanks for the catch go to Pat Lemm).

When the document was closed, you never got access to the Saved filename. So, I have updated the code here and it now works in all conditions and has been tested in Word 2013.

Here is how it works:

  1. Upon initialization you pass it your Word object.
  2. It attaches to the Before Save Event.
  3. When any save event occurs, it kicks off a thread that loops until the Background Save is complete.
  4. Once the background save is done, it checks to see if the document Saved == true:
  • If Saved == true: then a regular save did occur.
  • If Saved == false: then it had to be an AutoSave

In each case it will fire a unique event:

  • AfterSaveUiEvent
  • AfterSaveEvent
  • AfterAutoSaveEvent

Additionally, if the document being saved is also being closed, we catch the filename on the WindowDeactivate event on the way out. This can now be accessed by the caller (as you can see in the example below), to get the full filename of the closed document.

Here is the code to the class:

public class WordSaveHandler
{
    public delegate void AfterSaveDelegate(Word.Document doc, bool isClosed);
    // public events
    public event AfterSaveDelegate AfterUiSaveEvent;
    public event AfterSaveDelegate AfterAutoSaveEvent;
    public event AfterSaveDelegate AfterSaveEvent;

    // module level
    private bool preserveBackgroundSave;
    private Word.Application oWord;
    string closedFilename = string.Empty;

    /// <summary>
    /// CONSTRUCTOR  takes the Word application object to link to.
    /// </summary>
    /// <param name="oApp"></param>
    public WordSaveHandler(Word.Application oApp)
    {
        oWord = oApp;
        // hook to before save
        oWord.DocumentBeforeSave += oWord_DocumentBeforeSave;
        oWord.WindowDeactivate += oWord_WindowDeactivate;
    }
    
    /// <summary>
    /// Public property to get the name of the file
    /// that was closed and saved
    /// </summary>
    public string ClosedFilename
    {
        get
        {
            return closedFilename;
        }
    }

    /// <summary>
    /// WORD EVENT  fires before a save event.
    /// </summary>
    /// <param name="Doc"></param>
    /// <param name="SaveAsUI"></param>
    /// <param name="Cancel"></param>
    void oWord_DocumentBeforeSave(Word.Document Doc, ref bool SaveAsUI, ref bool Cancel)
    {
        // This could mean one of four things:
        // 1) we have the user clicking the save button
        // 2) Another add-in or process firing a resular Document.Save()
        // 3) A Save As from the user so the dialog came up
        // 4) Or an Auto-Save event
        // so, we will start off by first:
        // 1) Grabbing the current background save flag. We want to force
        //    the save into the background so that Word will behave
        //    asyncronously. Typically, this feature is on by default,
        //    but we do not want to make any assumptions or this code
        //    will fail.
        // 2) Next, we fire off a thread that will keep checking the
        //    BackgroundSaveStatus of Word. And when that flag is OFF
        //    no know we are AFTER the save event
        preserveBackgroundSave = oWord.Options.BackgroundSave;
        oWord.Options.BackgroundSave = true;
        // kick off a thread and pass in the document object
        bool UiSave = SaveAsUI; // have to do this because the bool from Word
        // is passed to us as ByRef
        new Thread(() =>
        {
            Handle_WaitForAfterSave(Doc, UiSave);
        }).Start();
    }

    /// <summary>
    /// This method is the thread call that waits for the same to compelte.
    /// The way we detect the After Save event is to essentially enter into
    /// a loop where we keep checking the background save status. If the
    /// status changes we know the save is compelte and we finish up by
    /// determineing which type of save it was:
    /// 1) UI
    /// 2) Regular
    /// 3) AutoSave
    /// </summary>
    /// <param name="Doc"></param>
    /// <param name="UiSave"></param>
    private void Handle_WaitForAfterSave(Word.Document Doc, bool UiSave)
    {
        try
        {
            // we have a UI save, so we need to get stuck
            // here until the user gets rid of the SaveAs dialog
            if (UiSave)
            {
                while (isBusy())
                    Thread.Sleep(1);
            }

            // check to see if still saving in the background
            // we will hang here until this changes.
            while (oWord.BackgroundSavingStatus > 0)
                Thread.Sleep(1);
        }
        catch (ThreadAbortException)
        {
            // we will get a thread abort exception when Word
            // is in the process of closing, so we will
            // check to see if we were in a UI situation
            // or not
            if (UiSave)
            {
                AfterUiSaveEvent(null, true);
            }
            else
            {
                AfterSaveEvent(null, true);
            }
        }
        catch
        {
            oWord.Options.BackgroundSave = preserveBackgroundSave;
            return; // swallow the exception
        }

        try
        {
            // if it is a UI save, the Save As dialog was shown
            // so we fire the after ui save event
            if (UiSave)
            {
                // we need to check to see if the document is
                // saved, because of the user clicked cancel
                // we do not want to fire this event
                try
                {
                    if (Doc.Saved == true)
                    {
                        AfterUiSaveEvent(Doc, false);
                    }
                }
                catch
                {
                    // DOC is null or invalid. This occurs because the doc
                    // was closed. So we return doc closed and null as the
                    // document
                    AfterUiSaveEvent(null, true);
                }
            }
            else
            {
                // if the document is still dirty
                // then we know an AutoSave happened
                try
                {
                    if (Doc.Saved == false)
                        AfterAutoSaveEvent(Doc, false); // fire autosave event
                    else
                        AfterSaveEvent(Doc, false); // fire regular save event
                }
                catch
                {
                    // DOC is closed
                    AfterSaveEvent(null, true);
                }
            }
        }
        catch { }
        finally
        {
            // reset and exit thread
            oWord.Options.BackgroundSave = preserveBackgroundSave;
        }
    }

    /// <summary>
    /// WORD EVENT – Window Deactivate
    /// Fires just before we close the document and it
    /// is the last moment to get the filename
    /// </summary>
    /// <param name="Doc"></param>
    /// <param name="Wn"></param>
    void oWord_WindowDeactivate(Word.Document Doc, Word.Window Wn)
    {
        closedFilename = Doc.FullName;
    }

    /// <summary>
    /// Determines if Word is busy  essentially that the File Save
    /// dialog is currently open
    /// </summary>
    /// <param name="oApp"></param>
    /// <returns></returns>
    private bool isBusy()
    {
        try
        {
            // if we try to access the application property while
            // Word has a dialog open, we will fail
            object o = oWord.ActiveDocument.Application;
            return false; // not busy
        }
        catch
        {
            // so, Word is busy and we return true
            return true;
        }
    }
}

 

And here is how you set it up and attach to it’s events:

public partial class ThisAddIn
{
    WordSaveHandler wsh = null;
    private void ThisAddIn_Startup(object sender,
                                    System.EventArgs e)
    {
        // attach the save handler
        wsh = new WordSaveHandler(Application);
        wsh.AfterAutoSaveEvent += new WordSaveHandler.AfterSaveDelegate(wsh_AfterAutoSaveEvent);
        wsh.AfterSaveEvent += new WordSaveHandler.AfterSaveDelegate(wsh_AfterSaveEvent);
        wsh.AfterUiSaveEvent += new WordSaveHandler.AfterSaveDelegate(wsh_AfterUiSaveEvent);
    }

    void wsh_AfterUiSaveEvent(Word.Document doc, bool isClosed)
    {
        if (!isClosed)
            MessageBox.Show("After SaveAs Event");
        else
            MessageBox.Show("After Close and SaveAs Event. The filname was: " + wsh.ClosedFilename);
    }

    void wsh_AfterSaveEvent(Word.Document doc, bool isClosed)
    {
        if (!isClosed)
            MessageBox.Show("After Save Event");
        else
            MessageBox.Show("After Close and Save Event. The filname was: " + wsh.ClosedFilename);
    }

    void wsh_AfterAutoSaveEvent(Word.Document doc, bool isClosed)
    {
        MessageBox.Show("After AutoSave Event");
    }

14 thoughts on “[UPDATED] Word After Save Event

  1. Thanks Dave, Excellent Post, But I found a pattern that your code does not support. For exemple, if you try to do a “Save As” on a document that required authentication on a network drive, sharepoint etc… Before the save as dialog open, Microsoft word show you the windows security modal window. Because this window is probably call using the “show” command, your validation using “object o = oWord.ActiveDocument.Application;” in the isbusy function is suddenly valid. So, if the user push the cancel button of the windows security modal windows, he did not save is document, but your code ran any way. I’m trying to figure out a way to make your code work in this situation, but so far, i did not find anything. Do you have any suggestion for me?

  2. Hi Dave,
    Your example is the closest I’ve came to solving my issue. But do you know if there there was a way to tell what option the user chooses from the SaveUi window (“Save”, “Cancel Save”, “Cancel”)?

    • The SaveAsUI tells you if the UI appeared, but not what the user clicked. If the user clicked Cancel and SaveAsUI is true, then we infer it in the code:

      if (UiSave)
      {….
      if (Doc.Saved == true)
      {
      // we can infer a Save happened, so user clickked OK.
      AfterUiSaveEvent(Doc, false);
      }
      else
      {
      // if we are here, the document is still dirty
      // because it was a UISave, we know the dialog appeared.
      // therefore the user must have clicked CANCEL
      UserClickedCancelSaveAsEvent(…);
      }
      ….
      }

  3. Thank you for sharing this! I am trying to adapt this to VSTO / VB.Net. A little confused with this code in the DocumentBeforeSave… any suggestions on how to convert this part to VB?

    new Thread(() =>
    {
    Handle_WaitForAfterSave(Doc, UiSave);
    }).Start();
    }

  4. Wow it is exactly what I needed but I still have some problem because when I’m closing document I can catch filepath because of WindowDeactivate Event but it is on saving state so I can’t do anything with it.

    In my example I need to Save file with XML file in it.

    So I need to save file , make copy, prepare copy with xml , close my active document , replace it. It is work when it is about normal saving but when I try to make it with Word close it’s making problems.

    At WindowDeactive Event already Document is closed so all documents is closed but my active document is in saving process so I can’t make o replace of it. I’m looking how to catch finish of saving event after WindowDeactive. Any Idea ?

    • You need something like an AfterCloseEvent.

      In the BeforeClose event, grab the file name. Save the document if it needs to be saved, and you will need to handle all prompts to the user here. One you determined it is saved and user wants to close, start a thread and close.

      The thread will not fire until after your document is completely closed. At this point you call a method like: AfterDocumentClose. You pass it the file name you grabbed earlier.

      At this point you can attempt to access the document with a FileStream object and keep looping until it succeeds. You do this just in case another program like the antivirus decides to scan it.

      Then at this point your file is completely closed and you can do whatever you need to.

  5. So in case of my needs I can’t use this example because I tried to put this AfterCloseHandler in your function like this

    void wsh_AfterSaveEvent(Word.Document doc, bool isClosed)
    {
    if (!isClosed)
    {
    SavingDoc(doc.FullName,doc.Name,true);
    MessageBox.Show(“After Save Event”);
    }
    else
    {
    MessageBox.Show(“Check The filname was: ” + wsh.ClosedFilename);
    string copyPath = SavingDoc(wsh.ClosedFullFileName, wsh.ClosedFilename, false);
    while (isDocClosed(wsh.ClosedFullFileName))
    Thread.Sleep(1);
    try
    {
    File.Copy(copyPath, wsh.ClosedFullFileName, true);
    }
    catch(Exception e)
    {
    Debug.WriteLine(“Error = “+e);
    }

    MessageBox.Show(“After Close and Save Event. The filname was: ” + wsh.ClosedFilename);
    }
    }

    private string SavingDoc(string docPath,string docName,bool forceToClose)
    {
    //Rzutowanie do zmiennej ścieżki do “Home” w zależności od systemu
    string homePath = (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX)
    ? Environment.GetEnvironmentVariable(“HOME”) : Environment.ExpandEnvironmentVariables(“%HOMEDRIVE%%HOMEPATH%”);

    string copyPath = homePath + “\\CustomXML\\” + docName;

    string staticPath = docPath;

    try
    {

    Directory.CreateDirectory(homePath + “\\CustomXML\\”);

    //Zapis kopii pliku do CustomXML
    File.Copy(staticPath, copyPath, true);

    SaveToXML(copyPath);
    if (forceToClose)
    {
    Globals.ThisAddIn.Application.Documents.Close();
    File.Copy(copyPath, staticPath, true);

    Globals.ThisAddIn.Application.Documents.Open(staticPath);
    }

    }
    catch (Exception e)
    {
    MessageBox.Show(“Zapis do pliku nie powiódł się.”, “Zapis nie powiódł się”, MessageBoxButtons.OK, MessageBoxIcon.Error);
    Debug.WriteLine(“Błąd: ” + e);
    }

    return copyPath;
    }

    private bool isDocClosed(string fileName)
    {
    try
    {

    FileStream zipToOpen = new FileStream(fileName, FileMode.Open);
    Debug.WriteLine(“After Check”);
    return false;
    }
    catch
    {
    Debug.WriteLine(“Checking”);
    return true;
    }
    }

    When I’m doing like this when my handler is running it is all time saving and not finish. So at all I can’t do it even if my handler let me go. I tried also with BeforeCloseEvent but it fire in the same time with BeforeSaveEvent and crush.

    What should I change to make it.

    Debug Output :

    After Check
    Error = System.IO.IOException: Proces nie może uzyskać dostępu do pliku „C:\Users\boche\Desktop\54654.docx”, ponieważ jest on używany przez inny proces.
    w System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
    w System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)
    w Documento.ThisAddIn.wsh_AfterSaveEvent(Document doc, Boolean isClosed) w D:\Documento\Project\Iceo.Documento\Documento\ThisAddIn.cs:wiersz 124

  6. From yesterday I’m trying also with your AfterClose Example but it is also not helping.

    I’m lost I can’t find any solution how to replace my original file to my copy with prepared xml

    • It is hard to tell from your code, but you seem to be closing and then opening the file in the save event. First, set a global Boolean that you are in save so your close code will check it and not fire. Additionally, you might look at writing a second addin which handles the open via some type of message. Meaning, you close, update the xml, save, turn on the other addin, drop it a message somewhere to look for it, like registry with file path to open. It handles the open, then the open event in this addin catches the open, identifies the file and then disables the secondary addin. It is a bit complicated, but should work and only way I can see you get around some of the issues.

  7. Hi Dave,

    So are you saying that for me to check if Word is currently running a AutoSave – all I would need to do is check the UiSave?

    if (UiSave)
    {
    // autosaving event
    else
    // regular save event
    }

    I am trying to find a way to determine, for certain that an AutoSave is in progress – the best solution I have found to date is using [WordApp.WordBasic.IsAutoSaveEvent].

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s