Opening an office document from Code

It’s a simple job at a glance, and it should be. But we had a bug with that feature. You open a document on Huddle and it should be opened in an application that is associated with the file, for example, MS Word, if it is word document. You can do that if you have installed Huddle for Windows, which is a desktop application for Huddle.

It worked. Yet, customers often complained that the document opened in the background. When we open any document, it opens in the foreground on dev machine. so you are very tempted to say “it works on my machine!”

This is the code that opens the document. No magic, just simple process.Start()

var process = new Process
{
    StartInfo =
    {
        FileName = path,
        CreateNoWindow = true,
        UseShellExecute = true,
        LoadUserProfile = false,
        ErrorDialog = false,
        Verb = "open"
    },
    EnableRaisingEvents = true
};

process.Start();

The application that’s launched is supposed to be active in the foreground, and I’ve got that behaviour on my dev machine. Yet on non-dev machines, especially our product manager’s laptop, the document opened consistently in the background, especially behind Chrome browser when it was maximised.

So, we made a win api call to set it in the foreground.


[DllImport("User32.dll")]
private static extern Int32 SetForegroundWindow(IntPtr hWnd);

...
var handle = process.MainWindowHandle
SetForegroundWindow(handle);

Now it was working on one of my VMs (Virtual Machines) and I thought it should be working. However it was still failing on a laptop.
Why? A little more investigation reveals that process.MainWindowHandle doesn’t return the handle immediately. When MS Word splash screen pops up, the process to MS Word exists, but the main window is not fully loaded yet, so the handle was IntPtr.Zero. You have to wait until MainWindowHandle is populated, and it takes 3 to 6 seconds and up to 10 seconds, if you open Word first time on the day.

So, the last missing bit was waiting for the handle.

int threshold = 0;
while (process.MainWindowHandle == IntPtr.Zero)
{
    if (threshold > 100) break; 

    _log.DebugFormat("Waiting for the document is fully loaded... - {0}", threshold);
    Thread.Sleep(TimeSpan.FromMilliseconds(100));
    threshold++;
}

_log.DebugFormat("Bringing the application (handle: {0}) to the front", process.MainWindowHandle);
SetForegroundWindow(handle);

And happy developers and customers, finally

Update as of Fri.19/12/2014

The journey didn’t end there unfortunately. The issue was deeper than I previously assumed.

Opening documents in the background had a mixture of causes.

  1. If there’s any Excel document, Excel opens all subsequent documents are in the same instance, just flashing the icon since Windows 7
  2. Event SetForegroundWindow doesn’t work, if the process that’s running the command is not in the foreground. So you have to set your process foreground first, if you want to set any other process you spawn foreground.

To sort them out, I took a different approach. First, I gave up the simple and elegant Process.Start() with UseShellExecute option. Instead, I query windows registry and find the associated application with the file extension.

[Flags]
public enum AssocF : uint
{
    None = 0,
    Init_NoRemapCLSID = 0x1,
    Init_ByExeName = 0x2,
    Open_ByExeName = 0x2,
    Init_DefaultToStar = 0x4,
    Init_DefaultToFolder = 0x8,
    NoUserSettings = 0x10,
    NoTruncate = 0x20,
    Verify = 0x40,
    RemapRunDll = 0x80,
    NoFixUps = 0x100,
    IgnoreBaseClass = 0x200,
    Init_IgnoreUnknown = 0x400,
    Init_FixedProgId = 0x800,
    IsProtocol = 0x1000,
    InitForFile = 0x2000,
}

public enum AssocStr
{
    Command = 1,
    Executable,
    FriendlyDocName,
    FriendlyAppName,
    NoOpen,
    ShellNewValue,
    DDECommand,
    DDEIfExec,
    DDEApplication,
    DDETopic,
    InfoTip,
    QuickTip,
    TileInfo,
    ContentType,
    DefaultIcon,
    ShellExtension,
    DropTarget,
    DelegateExecute,
    SupportedUriProtocols,
    Max,
}

// from http://www.pinvoke.net/default.aspx/shlwapi/AssocQueryString.html
[DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
static extern uint AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, ref uint pcchOut);

public string GetApplicationPath(string extension)
{
    const int S_OK = 0;
    const int S_FALSE = 1;

    uint length = 0;
    uint ret = AssocQueryString(AssocF.None, AssocStr.Executable, extension, null, null, 
         ref length);
    if (ret != S_FALSE)
    {
        throw new InvalidOperationException("Could not determine associated string");
    }

    var sb = new StringBuilder((int)length); 
    ret = AssocQueryString(AssocF.None, AssocStr.Executable, extension, null, 
          sb, ref length);
    if (ret != S_OK)
    {
        throw new InvalidOperationException("Could not determine associated string");
    }

    return sb.ToString();
}

You get the application and run it with Process.Start. You pass the file name as argument. In case of office documents, you can add “/x” switch, so that it creates a new instance all the time, not reusing the existing one.

var extension = Path.GetExtension(path);
try
{
    string application = _windowManager.GetApplicationPath(extension);
    var process = new Process
    {
        StartInfo =
        {
            FileName = application,
            Arguments = GetArguments(application, path),
            WindowStyle = ProcessWindowStyle.Normal
        }
    };

    _log.Debug(string.Format("Starting {0}", path));

    var huddleWinHandle = Process.GetCurrentProcess().MainWindowHandle;
    _windowManager.MinimiseWindow(huddleWinHandle);
    _windowManager.BringToFront(huddleWinHandle);
    process.Start();
}
...

private string GetArguments(string application, string path)
{
    if (application.ToLower().Contains("microsoft"))
    {
        return "/x \"" + path + "\"";
    }

    return "\"" + path + "\"";
}

Why do you minimise the window of the current process before setting it foreground? For some reason, SetForegroundWindow didn’t work consistently, if you process main window is in normal mode, hidden behind other windows. When it was minimised programatically and set to foreground, it was always brought to the front.

So, to summarise how to open documents in the foreground,

  1. Set your application to the foreground first by minimising the window and then bringing it to the front
  2. Find the associated application with the file and start it as new instance.
  3. Before you start another process, make sure your process in the foreground first. Only foreground process can make an attached process run in the foreground. There are more rules in setting a process in the foreground.
Opening an office document from Code

Tools, libraries and patterns used at Huddle

Huddle uses extensive list of open source libraries and tools, and I love it.
This is the list of things I discovered so far.

Server side

Client-side

scripting, build, …

Testing

Tools

To be continued …

Tools, libraries and patterns used at Huddle