So, in addition to my last post, one of the more common features I am asked about for Word is an AfterSave event. If you have done much Office Developer work, you will know how handy this would be, if it were only available.Well, in working with a few customers over the years I have developed a class that you can use to detect an After Save event.
Here is how it works:
- Upon initialization you pass it your Word object.
- It attaches to the Before Save Event.
- When any save event occurs, it kicks off a thread that loops until the Background Save is complete.
- 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
Here is the code to the class:
{
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;
/// <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
oApp.DocumentBeforeSave += new
Word.ApplicationEvents4_DocumentBeforeSaveEventHandler(
oApp_DocumentBeforeSave);
}
/// <summary>
/// WORD EVENT – fires before a save event.
/// </summary>
/// <param name="Doc"></param>
/// <param name="SaveAsUI"></param>
/// <param name="Cancel"></param>
void oApp_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…
ThreadStart starter = delegate {
Handle_WaitForAfterSave(Doc, UiSave); };
new Thread(starter).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(Doc))
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
{
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>
/// Determines if Word is busy – essentially that the File Save
/// dialog is currently open
/// </summary>
/// <param name="oApp"></param>
/// <returns></returns>
private bool isBusy(Word.Document oDoc)
{
try
{
// if we try to access the application property while
// Word has a dialog open, we will fail
Word.Application oApp = oDoc.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:
System.EventArgs e)
{
// attach the save handler
WordSaveHandler 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");
}
void wsh_AfterSaveEvent(Word.Document doc, bool isClosed)
{
if (!isClosed)
MessageBox.Show("After Save Event");
else
MessageBox.Show("After Close and Save Event");
}
void wsh_AfterAutoSaveEvent(Word.Document doc, bool isClosed)
{
MessageBox.Show("After AutoSave Event");
}
Hi,
maybe this one:
while (isBusy(Doc)) Thread.Sleep(1);
should always happen, because I had a situation right now, when it failed without.
There was this ‘conversion warning’ dialog popping up, but it doesn’t come along
with SaveAsUI=true!
Many thanks for the code, very useful
Thanks alot for the code..It is exactly what i was looking for.
Working perfectly.
Can you tell me how can i achieve the same functionality in Excel.
Options.BackgroundSave is not available in excel application object.
Any alternative you might be aware of?
Many thanks
subscribe to BeforeSave
in the event handler do Doc.Save()
Cancel = true;
then goes your after save code….
Thanks a lot for the code. Is there a way to retrieve the file location in the wsh_AfterUiSaveEvent if the user close word, but still save it”s document? I try Doc.Path, but it’s not a good idea. It’s seem word has close the doc object. Any idea how I can do this?
You would want to modify the AfterSave event to pass a parameter and before it is called from the BeforeClose event, you pass the Doc.Path as a string to the AfterSave event. My AfterWorkbookClose event for Excel (http://davecra.wordpress.com/2011/06/03/afterclose-event/) demonstrates this behavior.
Thanks for the quick response. In order to reproduce my issue, try the following.
1. Open microsoft word.
2. Type anything in the word document.
3. Press the window close button (At this moment, the Word_DocumentBeforeClose is call, but, since the file has never been save anywhere, the doc.Path is empty. So, word will open a save dialog)
4. Save the file anywhere on your machine
5. Try to retrieve the location specify in the save dialog.
In the Handle_WaitForAfterSave method, it’s no longer possible to retrieve where the user save his file, since the Doc property seem to be empty.
Maybe i’m missing something?
Best regards.
It has been a long time since I looked at this code. I worked with it, I believe in Word 2007. While reviewing your issue, I found a few things that needed to be corrected, especially to get this to work properly in Word 2013 and to be able to catch all scenarios. The answer your question on getting the filename, the easiest way is to attach to the WindowDeactivate event. It is the very last event to fire as a document is going out of memory. From here we can get an accurate filename. I save it in a private string and then added a property to the class called ClosedFilename. On a close event where the resultant value returned for isClosed == true, I can then query that property and get the filename.
I have updated the entire class in a new post:
http://davecra.wordpress.com/2013/04/26/updated-word-after-save-event/
Please let me know if this helps.
[…] 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 […]
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].
The WordBasic command was added to the product a few years ago (maybe 4 now) for exactly that purpose. So that is what I would use now versus this method. I think I blogged about it a while back. If not I will make sure to do so.
Thanks for the reply Dave, I was under the impression that the WordBasic command was really old and no longer advised (no more support etc.). Could you please share the blog link if you have one?
Actually, this was added to the Object Model very recently ~2012. While WordBasic itself is no longer supported and WordBasic is also not even advertised, this particular command was added to the WordBasic set so that the Word team would not have to update the overall Object Model. There is a whole rabbit hole there… Suffice to say, it is supported and was added recently just for this very reason. I detail it all here: http://davecra.com/2013/06/12/update-autorecovery-save-autosave-fires-off-the-documentbeforesave-event-in-word/
Also, you might check out some of my later code on this. This one entry was updated, but I also have an entire class I developed that works across all the applications in a similar method. Here: http://davecra.com/2014/07/08/office-wide-after-save-as-event-and-tangent-on-extension-methods-and-lambdas-in-office-code/
This is a great blog – keep up the good work!
Thank you, Dave.
COM (not System.Windows.Forms) IMessageFilter is a cleaner approach to your isBusy. You can remove your loop all together and next call to COM will wait until it’s free. https://blogs.msdn.microsoft.com/andreww/2008/11/19/implementing-imessagefilter-in-an-office-add-in/
I want achieve same thing in mac. Can U please guide me in this.
Unfortunately, I do not believe this can be done from a Mac. The VBA interface is the only development interface and it does not allow true multi-threading and the ability to fire an event after another event. The closest thing there is to that is the Application.OnTime() event. https://msdn.microsoft.com/en-us/library/office/ff820816.aspx. I am not sure if that is supported on the Mac, but that might be a step you can take from VBA if it does work.
Very nice work, thanks!! One question – In Word, I have an open document – I navigate in the ‘Save As’ dialog to a directory and select an exisiting file. When I now click ‘Save’ instead of ‘Cancel’, I get the message if I want to overwrite/merge the exisitng document. Is it possible to intercept the ‘Save’ event in the ‘Save As’ Dialog so that I can change the file name of the open document, suppressing the overwrite/merge message? Any suggestions are greatly appreciated!
There is no way to catch the Save event from the Word / Built-in Save As Dialog button. If you need to catch that event, then you will need set the Cancel = true on the Save event and display your own System.Windows.Forms.SaveFileDialog. There you can catch the Save or Cancel button and appropriately handle the next operation.
Thanks for the info, Dave! I will give this a spin! Cheers!!