VSTO Solution: Prompting to Save Normal.dotm

When you are automating Microsoft Word you may get a message similar to the following that appears when trying to Quit() the application:

This file is in use by another application or user.

(C:\Users\…\Normal.dotm

image

This is followed by several more prompts, like the Save As dialog and then this:

Changes have been made that affect the global template, Normal.dotm. Do you want to save those changes?

image

Typically, this occurs when there are multiple instance of WINWORD.EXE in memory. The problem occurs when one of them does something to the application (like changing a setting, macro or style) and ultimately causes those changes to be saved to the normal template. Typically, the first instance that closes locks the file and this causes the subsequent instances to be flagged to prompt you to save the changes.

This issue has been detailed here:

http://support.microsoft.com/kb/918064

Well, this is an issue I encounter often in the field and I have created some code to help with this situation:

/// <summary>
/// The deactivate event is called before the application exits.
/// We cannot determine when we exit or not (specifically), but anytime
/// this event fires, we locate the normal template and flip the flag
/// in order to prevent the dreaded "Normal needs to be saved" prompt.
/// </summary>
/// <param name="Doc"></param>
/// <param name="Wn"></param>
void Application_WindowDeactivate(Word.Document Doc, Word.Window Wn)
{
    try
    {
        object objectIndex = 0;
        // loop through all the loaded templates
        for (int idx = 1; idx <= Application.Templates.Count; idx++)
        {
            objectIndex = idx;
            Word.Template t = Application.Templates.get_Item(
                    ref objectIndex);
            // look for normal
            if (t.Name.ToLower().Contains("normal.dotm"))
            {
                // found it – set the flag and leave the loop
                t.Saved = true;
                break;
            }
        }
    }
    catch { } // something bad happened – but we will ignore it
}

Essentially, you attach to the Window Deactivate event and look for the Normal.dotm in the Templates collection and set the flag to saved. We have to place this in the Deactivate event because the Normal.dotm prompt occurs before your add-in unload event. Therefore, on deactivate just before the application quits, the Normal template flag is flipped to prevent the prompt. Here is how you hook up this event:

// attach to the Window Deactivate event in Word
Application.WindowDeactivate += new Microsoft.Office.Interop.
        Word.ApplicationEvents4_WindowDeactivateEventHandler(
            Application_WindowDeactivate);

Assigning a “macro” to a Textbox in VSTO/C#

In Excel you have the ability to insert a textbox into your spreadsheet, right-click on it and then assign a macro. This can be handy because maybe you want to capture the user clicking on the textbox as to take a specific action. See below:

textbox macro

However, there is no way to do this from C#. I have seen a couple of solutions now where it calls for an XLAM (Excel Add-in) to be placed in the XLSTART folder that can be used to register these textbox clicks and then call into your VSTO DLL through COM.. a la early-binding.

Messy… Confused smile

One thing I tell customers all the time is to not mix and match VBA and VSTO. Choose one or the other. This is because you can get into some real sticky situations that are downright impossible to debug.

So, in order to get one customer off this VBA crutch and move their entire code base into VSTO/C#, I did the following:

  1. Created a Mouse Hook class that captures mouse events
  2. When a left mouse click is detected it fires an event that the caller hooks to.
  3. Once the event is tripped, it checks the Excel selection and ask the selection for it’s “name.”

NOTE: This property is not normally exposed in VSTO (for some reason). However, it is accessible via VBA so I know it is there. Therefore, to get at it, I have to pInvoke it from Excel.

Here is the code to the Mouse Event Handler class:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Reflection;

public class UserActivityHook
{
    private static int hMouseHook;
    private delegate int HookProc(int nCode, int wParam, IntPtr lParam);
    private HookProc MouseHookProcedure;
    public delegate void MouseEventHandler(object sender, MouseEventArgs e);
    public event MouseEventHandler OnMouseActivity;

    #region mouse constants
    const int HC_ACTION = 0;
    const int WH_MOUSE_LL = 14;
    const uint WM_MOUSEMOVE = 0x200;
    const uint WM_LBUTTONDOWN = 0x201;
    const uint WM_LBUTTONUP = 0x202;
    const uint WM_LBUTTONDBLCLK = 0x203;
    const uint WM_RBUTTONDOWN = 0x204;
    const uint WM_RBUTTONUP = 0x205;
    const uint WM_RBUTTONDBLCLK = 0x206;
    const uint WM_MBUTTONDOWN = 0x207;
    const uint WM_MBUTTONUP = 0x208;
    const uint WM_MBUTTONDBLCLK = 0x209;
    const uint WM_MOUSEWHEEL = 0x20A;
    const uint WM_MOUSEHWHEEL = 0x20E;

    #endregion

    #region "DLL imports"
    // Methods
    [DllImport("user32.dll")]
    private static extern int CallNextHookEx(int idHook, int nCode,
                                           int wParam, IntPtr lParam);

    [DllImport("user32.dll")]
    private static extern int SetWindowsHookEx(int idHook, HookProc lpfn,
                                        IntPtr hInstance, int threadId);

    [DllImport("user32.dll")]
    private static extern bool UnhookWindowsHookEx(int idHook);
    #endregion

    #region "Structures"
    [StructLayout(LayoutKind.Sequential)]
    public class MouseHookStruct
    {
        // Fields
        public int dwExtraInfo;
        public int hwnd;
        public POINT pt;
        public int wHitTestCode;
    }

    [StructLayout(LayoutKind.Sequential)]
    public class POINT
    {
        // Fields
        public int x;
        public int y;
    }
    #endregion

    /// <summary>
    /// Start hook upon initialization
    /// </summary>
    public UserActivityHook()
    {
        Start();
    }

    /// <summary>
    /// Remove the hook on close.
    /// </summary>
    ~UserActivityHook()
    {
        Stop();
    }

    /// <summary>
    /// Starts the hook for the mouse events
    /// </summary>
    private void Start()
    {
        try
        {
            if (hMouseHook == 0)
            {
                // setup a callback for the WinAPI to this thread
                // instance which is shared by Excel, therefore
                // we are hooking to the message pump for Excel
                // but specificaly looking for mouse events.
                // See the callback:
                //   MouseHookProc()
                //
                MouseHookProcedure = new HookProc(MouseHookProc);
                hMouseHook = SetWindowsHookEx(14, MouseHookProcedure, IntPtr.Zero, 0);
                // hook failed for some reason…
                if (hMouseHook == 0)
                {
                    this.Stop();
                    throw new Exception("SetWindowsHookEx failed.");
                }
            }
        }
        catch (Exception ex)
        {
            throw new Exception("SetWindowsHookEx failed: " + ex.ToString());
        }
    }

    /// <summary>
    /// Is called by Windows when a Mouse event occurs.
    /// This is the main hook procedure setup from the
    /// Start() function.
    /// </summary>
    /// <param name="nCode"></param>
    /// <param name="wParam"></param>
    /// <param name="lParam"></param>
    /// <returns></returns>
    public int MouseHookProc(int nCode, int wParam, IntPtr lParam)
    {
        try
        {
            // is there a code passed?
            // and is the caller hooked to our event
            if (nCode >= 0 && OnMouseActivity != null)
            {
                MouseButtons buttons1 = MouseButtons.None;
                // is it a mouse event
                if (wParam == WM_LBUTTONUP)
                {
                    buttons1 = MouseButtons.Left;
                }
                else if (wParam == WM_RBUTTONUP)
                {
                    buttons1 = MouseButtons.Right;
                }
                else
                {
                    buttons1 = MouseButtons.None;
                }

                // extract the mouse params/struct passed
                // from windows so we can invoke it
                int num1 = 1;
                MouseHookStruct struct1 =
                    (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));
                // cast into Mouse args and then invoke the event
                MouseEventArgs args1 = new MouseEventArgs(buttons1,
                                    num1, struct1.pt.x, struct1.pt.y, 0);
                OnMouseActivity.Invoke(this, args1);
            }
            return CallNextHookEx(hMouseHook, nCode, wParam, lParam);
        }
        catch
        {
            return 0; // fail silently here
        }
    }

    /// <summary>
    /// Turn off the hook.
    /// </summary>
    private void Stop()
    {
        try
        {
            bool flag = true;
            if (UserActivityHook.hMouseHook != 0)
            {
                // call API to unhook
                flag = UnhookWindowsHookEx(hMouseHook);
                hMouseHook = 0;
            }
            if (!flag)
            {
                throw new Exception("UnhookWindowsHookEx failed.");
            }
        }
        catch { } // ignore on fail
    }
}

To attach to the hook:

void hook_OnMouseActivity(object sender, System.Windows.Forms.MouseEventArgs e)
{
    try
    {
        if (e.Button == MouseButtons.Left)
        {
            object sel = Application.ActiveWindow.Selection;
            // we must pInvoke Excel to get the name property
            // from the selection since it is not exposed in
            // the PIA's…
            object Name = sel.GetType().InvokeMember("Name",
                    BindingFlags.GetProperty, null, sel, null);
            // cheap way to see if it is a textbox…
            if (Name.ToString().ToLower().Contains("text"))
            {
                MessageBox.Show("Got it: " + Name.ToString());
            }
        }
    }
    catch { }
}

Then to catch the event and determine what was clicked:

There it all is. Now the real beauty is that you can actually use this code to detect the user clicking on all sorts of things, not just a textbox. Winking smile

Word AfterSave Event

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. Hot smile

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

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;

    /// <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:

private void ThisAddIn_Startup(object sender,
                               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");
}

Auto Recovery Save (AutoSave) fires off the DocumentBeforeSave event in Word

As an Office Development Advisor for Microsoft Premier Field Engineering, I work with a lot of different customers. A lot! However, one item I come across almost every single time, and one I have dealt with as an Escalation Engineer in Product Support Services was that of the dreaded Auto Save Issue. Confused smile

This is the one where Auto Save fires off the Before Save event and there is no way to distinguish if it was a user initiated save or one that came from the built in and timed Auto Save.

Well, while working with a rather large customer last year, we were able to push for and get a small design change to Word that is nearly 15 years in the waiting. If you have Office 2007, you are already in luck. See the following article:

Macros in Office Word 2007 cannot differentiate between "Auto Recovery Save" and "Manual Save" in the DocumentBeforeSave event

(http://support.microsoft.com/kb/2193786)

NOTE: This fix however, is not going to be available in Office 2010 until Service Pack 1 releases (sometime this summer).

Now you VSTO folks don’t get all happy just yet. There is a real catch for you in C#. First sign is that you will notice the article does not have any source code for you. Surprised smile WHAT!!!!

Well, as it turns out thee is there is a small hitch. The design change places the IsAutosaveEvent flag off the WordBasic object. Yes, you read that correctly… the WORD BASIC object. Really!!!! Sick smile

And if you have ever tried to get a listing of properties and methods off the WordBasic object in VSTO/C#, you would quickly realize… there are none defined in the Office PIA’s. Oops. Disappointed smile

However, where there is a will, there is a way. Winking smile

And now, just for you C# folks, here is how you access that confounded flag:

object oBasic = Application.WordBasic;
// this is where we invoke the object and
// get the property. But we get an "object"
// back so be careful casting it.
object fIsAutoSave =
    oBasic.GetType().InvokeMember(
        "IsAutosaveEvent",
        BindingFlags.GetProperty,
        null, oBasic, null);

if (int.Parse(fIsAutoSave.ToString()) == 1)
    MessageBox.Show("Is AutoSave");
else
    MessageBox.Show("Is regular save");

 

I did not say it was pretty, but what you are doing here is bypassing the PIA’s and directly invoking the object itself. And it works. And it is safe – you just have to be careful with your casting.

So, there you have it. Happy coding!!! Open-mouthed smile

Exceptions Occur When Automating Excel / Detecting Cell Edit Mode

Sometimes when your are Automating Excel, you may see an exception similar to the following:

System.Runtime.InteropServices.COMException was unhandled
  Message="Exception from HRESULT: 0x800A03EC"
  Source="Microsoft.Office.Interop.Excel"
  ErrorCode=-2146827284

This may seem random and may not seem to occur on your system. Well, this is a fairly common issue I have experienced in the field. And, there is not a lot of good information about it on the web. Until now…

The problem occurs because Excel is in edit mode. The user has a cell selected and text currently being typed into it, but they have not yet clicked out of the cell.

Note, when this happens Excel grays out the Ribbon items and most options are unavailable.

Well, there really is not an easy way to determine this state for Excel, so  the following code can be used to determine if Excel is in Edit Mode and also to get it out of Edit mode. The only call you need to make is exitEditMode(). If Excel is OK, nothing happens; however, if it is in edit mode, it pops it out so your automation code can continue.

[DllImport("User32.dll")]
public static extern Int32 SetForegroundWindow(int hWnd);

Excel.Application xlApp;
public Form1()
{
    InitializeComponent();
    xlApp = new Excel.Application();
    xlApp.Visible = true;
}

private void button1_Click(object sender, EventArgs e)
{
    exitEditMode(); // the only call you need to make
}

private void exitEditMode()
{
    if (!isExcelInteractive())
    {
        // get the current range
        Excel.Range r = xlApp.ActiveCell;
        // bring Excel to the foreground, with focus
        // and issue keys to exit the cell
        xlBringToFront();
        xlApp.ActiveWindow.Activate();
        SendKeys.Flush();
        SendKeys.Send("{ENTER}");
        // now make sure the original cell is
        // selected…
        r.Select();
    }
}

private bool isExcelInteractive()
{
    try
    {
        // this line does nothing if Excel is not
        // in edit mode. However, trying to set
        // this property while Excel is in edit
        // cell mdoe will cause an exception
        xlApp.Interactive = xlApp.Interactive;
        return true; // no exception, ecel is
                     // interactive
    }
    catch
    {
        return false; // in edit mode
    }
}

private void xlBringToFront()
{
    SetForegroundWindow(xlApp.Hwnd);
}

Updating the Styles of Embedded Excel Tables in PowerPoint 2007/2010

PowerPoint 2007/2010 are great with updating your presentation with new styles or themes. Excel Charts, SmartArt objects and shapes are all updated with a click of a button. However, you may find that Excel Charts embedded in the presentation are not updating. Here is some sample code to get these to update…

VBA:

Code Snippet
  1. Sub UpdateExcelTables()
  2.     Dim sld As Slide
  3.     Dim sha As Shape
  4.     For Each sld In ActivePresentation.Slides
  5.         For Each sha In sld.Shapes
  6.             If sha.Type = msoEmbeddedOLEObject Then
  7.                 If InStr(1, sha.OLEFormat.ProgID, "Excel") Then
  8.                     Dim xlApp  As Excel.Application
  9.                     sha.OLEFormat.Activate
  10.                     Set xlApp = sha.OLEFormat.Object.Parent
  11.                     xlApp.Workbooks(1).ApplyTheme _
  12.                         "C:\Program Files\Microsoft Office\" & _
  13.                         "Document Themes 12\Flow.thmx"
  14.                     xlApp.Quit
  15.                 End If
  16.             End If
  17.         Next
  18.     Next
  19. End Sub

C#:

Code Snippet
  1. foreach(PowerPoint.Slide sld in pres.Slides)  
  2. {
  3.     foreach (PowerPoint.Shape sha in sld.Shapes)  
  4.     {  
  5.         if (sha.Type == MsoShapeType.msoEmbeddedOLEObject  
  6.             && sha.OLEFormat.ProgID.Contains("Excel"))  
  7.         {  
  8.             sha.OLEFormat.Activate();  
  9.             object o = sha.OLEFormat.Object;  
  10.             // "Parent" property is not exposed,  
  11.             // but this is how we get reference to  
  12.             // the Excel Application  
  13.             Excel.Application xlApp =  
  14.                 (Excel.Application)o.GetType().InvokeMember  
  15.                 ("Parent",BindingFlags.GetProperty,  
  16.                     null, o, null);  
  17.             // specify the full path and filename  
  18.             // of the theme you wish to apply here  
  19.             xlApp.Workbooks[1].ApplyTheme(  
  20.                 @"C:\Program Files\Microsoft Office\"+  
  21.                 "Document Themes 12\Flow.thmx");  
  22.             xlApp.Quit();  
  23.         }    
  24.     }
  25. }

IMPORTANT NOTE:

If you look at the C# code we are doing something that may look odd:

  1. Excel.Application xlApp =  
  2.     (Excel.Application)o.GetType().InvokeMember  
  3.     ("Parent",BindingFlags.GetProperty,  
  4.      null, o, null);

This does the same thing as the VBA code:

  1. Set xlApp = sha.OLEFormat.Object.Parent

So, it turns out that the Office PIA’s do not implement all of the functionality you might see from VBA. The property “Parent” is still in the Object Model, it is just not exposed to the C# programmer. So what we are doing is Invoking that object we know is there.

This is sort of a neat workaround to those cases where you find you can do something in VBA, but you do not see it in the PIA.