Opening a Microsoft Office documents with Silverlight

Situation

From our Silverlight application we must be able to view documents. Documents are stored in an existing document management system and are accessible through a WCF REST service. It sounds very easy, but it was quite hard to understand all the stuff going on behind the Internet Explorer and the Microsoft Office.

Simplest solution

Download the content via a WebClient instance and save it with help of the Silverlight SaveFileDialog to the users local disk. Unfortunately this simple solution provides not the expected user experience, because Silverlight is not able to set the file name for the SaveFileDialog and after saving the user must manually navigate to the local folder and open the document by hand. The user experience should like be “Click & View” and not “Click, Safe, Search, Open & View”.

Expected solution

Open a popup from the Silverlight application with the URL pointing to the document.

Via HtmlPage:

Uri docUri = new Uri("http://mydomain/docs/test.docx");
HtmlPage.PopupWindow(docUri, "windwow1", null);

Or via a HyperlinkButton:

<HyperlinkButton Content="open document"
                NavigateUri="http://mydomain/docs/test.docx"
                TargetName="_blank" />

The service part

We deliver documents through a simple WCF REST service from our middle layer infrastructure. We use a custom authentication system that relies on HTTP session cookies for client identification. When the user isn’t authenticated then the service will redirect the request to the logon page (status code 302). The REST service is a self-hosted windows service (existing middle layer). The service is available with the URI pattern: http://<myhost>/docs/<docname&gt;

As example: http://mydomain/docs/test.docx

Here the simplified version of our REST service.

public Stream GetContent(string docId)
{

    WebOperationContext context = WebOperationContext.Current;

    // read session cookie
    string cookies = context.IncomingRequest.Headers[HttpRequestHeader.Cookie];
    string sessionId = "";
    if (cookies != null)
    {
        // parse cookie values
        var cookieItems = from c in cookies.Split(';')
                            let cc = c.Split('=')
                            where cc.Length == 2
                            select new { Name = cc[0].Trim().ToLower(), Value = cc[1].Trim() };

        var items = cookieItems.ToDictionary(c => c.Name);

        if (items.ContainsKey("sessionid"))
            sessionId = items["sessionid"].Value;
    }

    // check session is valid
    bool sessionValid = false;
    sessionValid = sessionId == "1234"; // todo

    if (!sessionValid)
    {
        // redirect uri
        string redirectUri = "http://mydomain/logon.html";
        // ...set the location the logon site should naviagate to after the successful logon
        redirectUri += "?redirect=" + context.IncomingRequest.UriTemplateMatch.RequestUri.ToString();
        // redirect to the logon page 
        context.OutgoingResponse.Location = redirectUri;
        context.OutgoingResponse.StatusCode = HttpStatusCode.Redirect;
        // response has header only
        return null;
    }
    else
    {

        try
        {

            // check if document exists 
            bool docExists = true; // todo

            if (docExists)
            {
                // todo
                FileInfo requestedFile = new FileInfo("todo");

                // set mime type (firefox, chrome and safari requires them)
                context.OutgoingResponse.ContentType = GetMimeType(requestedFile.Extension);
                return new FileStream(requestedFile.FullName, FileMode.Open);
            }
            else
            {
                // doc not found
                context.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
                context.OutgoingResponse.StatusDescription = "Document not found";
                // response has header only
                return null;
            }

        }
        catch (Exception ex)
        {
            // general error
            context.OutgoingResponse.StatusCode = HttpStatusCode.InternalServerError;
            // response has header only
            return null;
        }
    }
}

At this point everything works fine with Firefox, Google Chrome and Safari on Mac.

Internet Explorer is different

Internet Explorer acts a little bit different as expected. When opening an office document (Word, Excel, PowerPoint) from a web page in Internet Explorer the Fiddler call stack lock like this:

winword:  HEAD    http://mydomain/docs/test.docx HTTP/1.1     405 NotAllowed
winword:  OPTIONS http://mydomain/docs/ HTTP/1.1              405 NotAllowed
winword:  GET     http://mydomain/docs/test.docx HTTP/1.1     302 Redirect

As you can see the iexplore process isn’t involved. What’s going on? IE detects the mime type from the file name extension, if it is preserved in the URL string. Here some examples:

 
http://mydomain/docs/test.docx IE detects mime type as docx
http://mydomain/service.svc/test.docx IE cannot detect mime type (it isn’t svc)
http://mydomain/resource.ashx?file=test.docx IE cannot detect mime type (it isn’t ash)

What happened with iexplore? Office 2007 and 2010 Beta are designed to make a more collaborative workspace. Therefore, several changes have been made to how Office works with web content. These changes provide better authoring features for the following Web servers that support Office:

  • Microsoft Windows SharePoint Services
  • Microsoft SharePoint Portal Server
  • Microsoft Exchange Web Store

IE detects if the web resource is an Office format by analyzing the URL. If so IE will start the corresponding Office program with the URL as process start parameter like the following:

“C:\Program Files\Microsoft Office\Office14\WINWORD.EXE” /n http://mydomain/docs/test.docx”

Office is now downloading the content from the web:

image

The request will end with the status code 302 and a redirect location to our logon page, because session cookies are not shared between Internet Explorer and winword process. Well, Office Word will follow this redirection and tries to display our Silverlight logon page. This isn’t possible because the SL plug-in isn’t available for the Office suite and Word shows the following content.

image 

When we change our service so that it doesn’t make a redirection and returning the status code 404 (Not Found), then word is telling me that it wasn’t able to open the doc.

image

First improvement

Changing the service to get another URL pattern for our documents so that IE cannot detect the mime type through the URL.

Something like this.

What’s happening now. Let’s look at Fiddler’s call stack.

iexplore: GET     http://mydomain/docs/test.docx HTTP/1.1     200 OK
winword:  HEAD    http://mydomain/docs/test.docx HTTP/1.1     405 NotAllowed
winword:  OPTIONS http://mydomain/docs/ HTTP/1.1              405 NotAllowed
winword:  GET     http://mydomain/docs/test.docx HTTP/1.1     302 Redirect

Now IE is downloading the content as expected. But then winword comes into the play again. What’s going on in this situation?

IE cannot detect the mime type from the URL so it will download the web content as any other browser. With the request to our service the browser is sending our session cookie to the server too. Based on the mime type from the response IE decides to open Microsoft Word. IE does that the same way as before: starting the winword process with a startup argument including the URL instead of the downloaded local file.

“C:\Program Files\Microsoft Office\Office14\WINWORD.EXE” /n http://mydomain/docs/test.docx”

Same situation, Office looks like this again:

image 

Why the Office suite is doing that? Because Office lets you edit and author documents on a Web site if the server supports Web authoring and collaboration (Sharepoint, Exchange, etc). First, Office tries to communicate with the Web server with a series of HEAD and OPTIONS requests to discover the possibilities of the webserver (“Microsoft Office Protocol Discovery”, “Microsoft Office Existence Discovery” and “Microsoft Office Core Storage Infrastructure”). Then Office tries to directly bind to the resource with a GET request to the web resource.

Second improvement

Our WCF REST service doesn’t handle the HEAD and OPTIONS request from the “Microsoft Office Protocol Discovery” and “Microsoft Office Existence Discovery”, but it redirects the GET request from the “Microsoft Office Core Storage Infrastructure” to the logon page while winword can’t deliver the session cookie.

What we can do is to detect the caller via the User-Agent header. There are three main User-Agent’s used from the Office suite.

  • Microsoft Office Protocol Discovery
  • Microsoft Office Core Storage Infrastructure
  • Microsoft Office Existence Discovery

In case of one of these 3 user-agents our service returns the status code 401 (Unauthorized) instead of the 302 (Redirect).

Then winword ignores the 401 and opens the document from the cached document which was previously downloaded from IE.

Here the code snippet to do that:

// check user agent for office product suite
bool isOfficeSuite = false;
if (!string.IsNullOrEmpty(WebOperationContext.Current.IncomingRequest.UserAgent))
{
    string[] officeUserAgents = { "Microsoft Office Protocol Discovery",
                                    "Microsoft Office Existence Discovery",
                                    "Microsoft Office Core Storage Infrastructure" };

    string requestUserAgent = WebOperationContext.Current.IncomingRequest.UserAgent.ToLower();

    var q = from userAgent in officeUserAgents
            where requestUserAgent.ToLower().Replace(" ", "")
                               .Contains(userAgent.ToLower().Replace(" ", ""))
            select userAgent;

    isOfficeSuite = q.Count() > 0;
}

if (isOfficeSuite)
{
    // don't redirect when office program want getting the document via 
    // "Microsoft Office Protocol Discovery" or 
    // "Microsoft Office Core Storage Infrastructure" requests.
    // Excel/word whould redirect to the logon page an display the html!
    WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Unauthorized;
}
else
{
    // redirect to the logon page
    WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Redirect;
    // TODO
    WebOperationContext.Current.OutgoingResponse.Location = "http://rnd.glauxsoft.ch/evidencenovaweb/";
}

// response has header only
return null;

 

Unfortunately this works well with Office 2007, but first tests with Office 2010 Beta are different again. Excel and PowerPoint 2010 doesn’t ignore the 401 status code and telling us “Could not open the document …”. Maybe this is an error in the beta.

Third improvement

However, this kind of communication doesn’t make me happy. What can we do that IE downloads the content and then simply start the process associated to the mime type as that other browsers still do?

The typical workaround I found is to use the Content-Disposition attachment header in the GET response when returning the file. This header will tell the web browser to treat the file as a download (read-only), so the file will open in Office from the web browser cache location instead of a URL. With that setting, the Office application will treat the file as local, and will therefore not make calls back to the web server.

Content-disposition is an extension to the MIME protocol that instructs a MIME user agent on how it should display an attached file. When Internet Explorer receives the header, it raises a File Download dialog box whose file name box is automatically populated with the file name that is specified in the header

In our service we set this header for all well-known Office formats, because other mime types should still be opened inline within the browser such as PDF, TXT or JPG, GIF, etc.

Here the code snippet to doing that

// well-know office formats
string[] officeMimeTypes =  {  ".doc",".dot",".docx",".dotx",".docm",".dotm",
        ".xls",".xlt",".xla",".xlsx",".xltx",".xlsm",
        ".xltm",".xlam",".xlsb",".ppt",".pot",".pps",
        ".ppa",".pptx",".potx",".ppsx",".ppam",".pptm",
        ".potm",".ppsm"};

// add content-disposition header.This header will tell the web browser 
// to treat the file as a download (read-only), so the file will open 
// in Office from the web browser cache location instead of a URL. 
if (officeMimeTypes.Contains(requestedFile.Extension.ToLower()))
{
    WebOperationContext.Current.OutgoingResponse.Headers.Add("Content-disposition",
                                            "attachment;filename=" + requestedFile.Name);
}

// set mime type (firefox, chrome and safari requires them always)
WebOperationContext.Current.OutgoingResponse.ContentType = GetMimeType(requestedFile.Extension);
return new FileStream(requestedFile.FullName, FileMode.Open);

 

Summary

Internet Explorer handles Microsoft Office formats other than expected. The main reason is to make a more collaborative workspace when working with SharePoint and Exchange. This different behavior let you run into troubles when you have your own service providing the documents.

In brief you should consider the following to get around these troubles:

  • Don’t let the IE detect the mime type from the URL
  • Don’t redirect to the logon page when the User-Agent is “Microsoft Office Core Storage Infrastructure”
  • Set the Content-disposition header for all well-known Office formats when returning the file.

Resources

http://support.microsoft.com/kb/260519/en-us

http://support.microsoft.com/default.aspx?scid=kb;EN-US;899927

http://blogs.msdn.com/vsofficedeveloper/pages/Office-Existence-Discovery-Protocol.aspx

About these ads
Tagged , ,

18 thoughts on “Opening a Microsoft Office documents with Silverlight

  1. [...] Read the original post: Opening an Microsoft Office documents with Silverlight « Kiener's Blog [...]

  2. [...] This post was mentioned on Twitter by SilverlightCommunity, Winson Tang. Winson Tang said: Opening a Microsoft Office documents with Silverlight http://bit.ly/a4mgWD [...]

  3. [...] Opening a Microsoft Office document with Silverlight [...]

  4. [...] and the WScript object, for example. You can also use the browser to do this – see here for an interesting case study from Beat Kiener, who does this with remote [...]

  5. Mike Hambidge says:

    Beat,

    Great post! I’ve recently been working on a web application with respect to Internet Explorer and Office 2007′s handling of downloaded files and am seeing something odd.

    The webapp properly sets the Content-disposition so that the file will open in Office from the web browser cache location. The weirdness occurs when I attempt to “Save” the file that is open. If the file is an .xlsx Excel spreadsheet the save fails with a sharing violation.

    It seems as if Internet Explorer is holding a handle on the file in the browser cache. If I close Internet Explorer the save works successfully. I see similar “bad” results when downloading/opening Office 2003 .doc and .xls files in Office 2007.

    Have you seen similar behavior in your testing?

    • beatkiener says:

      Hi Mike,
      No, I haven’t seen a similar behavior. This week I have to make some changes on exactly this component. I will check that scenario too and I will give you feedback.

    • beatkiener says:

      Now I’ve detected the same issue when opening a xlsx document from IE in Office 2010. As long as the internet explorer is open the temporary file is locked by it. Office provides me the following message: When saving a file, a sharing violation can be caused by the way an antivirus application interacts with Microsoft Office. Check with your antivirus provider to make sure that the current version of the application, including updates, is installed. The antivirus provider might also have information or solutions for sharing violation issues.

      Interestingly is that only excel has this problem.

      • Mike Hambidge says:

        Thanks for testing this out. Depending on the version and service pack level of Microsoft Office and Internet Explorer you will see different behavior. In Office 2007 SP 1 the documents open Read Only (as you would expect). Starting with Office 2007 SP2 and going forward we starting seeing the funky behavior.

        I ended up using Process Explorer to view the file handles open to the spreadsheet and found that Internet Explorer indeed keeps a handle open to the document. However, the handle does finally go away after around 60 seconds. (Perhaps some background process is taking place in IE that finishes or times out after a while). So, if you wait 60 seconds after opening the file you can then “save” it without getting the error.

        Having said all that… I still feel its a bad user experience to allow users to save to a file located in IE’s temporary browser cache. First the cache will eventually be clearer. Second average users won’t have any clue where the cache is located.. much less be able to enable display of system/hidden directories so that they can even see it. Ultimately you would want it to open read-only and force the user to perform a “Save-As”.

        It sounds to me like something “got broken” in Office along the way and nobody noticed it.

  6. NC says:

    Hi Beat,

    That is a great article and will be of much use to me.

    Is it possible to get the same effect or end result if the office document is inside the XAP file as a resource or content? The Silverlight application I am working on will be used offline and will not be able to access a Web Service / WCF service.

    Thank you
    NC

    • beatkiener says:

      Hi NU,

      Well, since you are using your app offline it must run as Out-of-Browser App, right? Then you are able to use COM-Automation, which allows you to save the file anywhere on the disc on open it from code via Com-Automation. The only limitation I see so far is that this will not work on a Mac, but on there you can provide an alternative solution with the SafeFileDialog or HyperLink.
      Here is my sample code which is working fine on a Windows machine.
      Please let me know if it is working on a Mac too. Your feedback will be highly appreciated.


      private void OnSaveFile(object sender, RoutedEventArgs e)
      {
      if (!Application.Current.HasElevatedPermissions)
      {
      MessageBox.Show("Please install this application in order to use this functionality.");
      return;
      }
      try
      {
      // create folder in users document directory
      string path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
      DirectoryInfo dir = Directory.CreateDirectory(System.IO.Path.Combine(path, "OpenFileInOOB"));

      // name of resource file
      string fileName = "Test.docx";

      // get resource stream
      Uri resUri = new Uri(
      string.Format("/{0};component/{1}",
      App.Current.GetType().Assembly.FullName.Split(',')[0], fileName), UriKind.Relative);
      var stream = Application.GetResourceStream(resUri);

      if (stream != null)
      {
      // save file into the users documents folder
      string fullPath = System.IO.Path.Combine(dir.FullName, fileName);
      Stream file = new FileStream(fullPath, FileMode.Create);
      stream.Stream.CopyTo(file);
      file.Flush();
      file.Close();

      if (AutomationFactory.IsAvailable)
      {

      // launch the associated program via the file path
      dynamic cmd = AutomationFactory.CreateObject("WScript.Shell");
      cmd.Run(fullPath);
      }
      else
      {
      // other approach when using a Mac
      // by using SafeFileDialog
      HyperLinkHelper link = new HyperLinkHelper();
      // i don't know whether 'file:///' will work on a mac or not
      link.NavigateUri = new Uri("file:///" + fullPath);
      link.TargetName = "_blank";
      link.Click();
      }
      }
      else
      {
      MessageBox.Show("Resource not found");
      }

      }
      catch (Exception ex)
      {
      MessageBox.Show(ex.Message);
      }

      }

      private class HyperLinkHelper : HyperlinkButton
      {
      public void Click()
      {
      base.OnClick();
      }
      }

      Here you can find my demo project.

      • NC says:

        Hi Beat,

        Thank you for your time and apologies for being vague. When I said, the application runs offline, I meant it will be triggered by a HTML sitting beside the XAP. The application will not be running out of the browser.

        Thank you
        NC

      • beatkiener says:

        Okay, no problem. Then I think it should be the same experience as when downloading from a webserver. You can just provide a “file”-Url instead “http”-Url and reference the resource which is sitting beside the XAP.
        Note: you can grab the file path from the Application.Current property.

        Thank you for your feedback
        Beat

      • NC says:

        Due to the security restrictions and sandboxing of silverlight runtime, we cannot set a “file”-url. Is there a workaround to do that?

        Thank you
        NC

      • beatkiener says:

        You are right. I played around with several javascript hacks in order to call some javascript function to open the document, but I wasn’t successful.
        Currently I think there is no way to open a local file without having a webserver in between.

        Please let me know if you find a solution, I’m also very interested in that.

        Thanks for your feedback.

  7. mustbegonge says:

    Spire.Doc can open a local file without having a webserver in between.

    http://www.e-iceblue.com/Introduce/word-for-net-introduce.html

  8. I enjoy what you guys tend to be up too. This sort of clever work and
    coverage! Keep up the good works guys I’ve you guys to my own blogroll.

  9. Bob B says:

    Hi Beat,

    Your original topic discussion under “Third Improvement” implies a correlation with an extremely irritating issue we’ve had since installing Silverlight. We use MS “Live” webmail on machines with XP Pro OS and IE8 and prior to installation of Silverlight we had no problems attaching open MS Office 2007 files to our emails. Now when attempting to attach any open MS Office file to a Live email, we receive the following error message “Files with errors will not be uploaded”. No other file types (PDF, JPG, TXT, BMP, etc) seem to be affected. The extent of MS tech support help is pretty much limited to suggestions to “Optimize your browser” and/or “Update your OS & IE”.

    I recognize that your blog is not intended to provide tech support but any insight you might be willing to provide would be very much appreciated as you certainly seem to be more knowledgeable about interaction and conflicts between these MS programs than MS tech support.

  10. CodeGuyRoss says:

    Is there a working version of this example somewhere.
    I am trying to reproduce the result but the following command doesn’t appear to work:
    WebOperationContext.Current.OutgoingResponse.Headers.Add(“Content-disposition”,
    “attachment;filename=” + requestedFile.Name);

    This is listed as a silverlight example but I do not see Outgoingresponse.Headers.Add function in silverlight. I checked the microsoft documentation at http://msdn.microsoft.com/en-us/library/system.servicemodel.web.outgoingwebrequestcontext(v=vs.95).aspx and I see a headers property but it does not have an add function.

    Any help would be appreciated!

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

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: