This is part three (and counting) of my Building your own WOPI Client series. In part 1 I discussed the WOPI Protocol and especially how to implement the Discovery process of a WOPI Client. In part 2 we built a viewer application as a WOPI Client and connected it to SharePoint. In this part we’re modifying our WOPI Client to support basic editing of the files.

Modyfing the WOPI Client Discovery data

The first thing that we need to do is to modify our Discovery method, in our case the static XML file, to tell the WOPI Server that we support editing of the files. It’s a simple procedure and we just add another line of XML like this to the app element:

<action name="edit"
    ext="cs"
    default="false"
    urlsrc="http://wacky.corp.local/csharp/editor.aspx?&lt;theme=THEME_ID&amp;&gt;" />

This action has the name set to edit, to tell the WOPI Server that we now support editing of cs files. The default attribute is set to false (the view mode is the default one) and we set the urlsrc to point to our editor.aspx file (that we’ll build in a minute).

Once this is done, the WOPI Server will not pick that change up automatically. Instead we need to manually refresh the WOPI Bindings in SharePoint by removing them and then add them back again:

Remove-SPWOPIBinding -Server wacky.corp.local
Add-SPWOPIBinding -Server wacky.corp.local -AllowHTTP

An IISRESET might be necessary for SharePoint to pick this up.

Modifying the Viewer

To be able to switch to Edit mode from the viewer we slightly update the user interface to include an Edit button so that it looks like below. As you can see I also added a Close button, which just return the user to the Document Library where the file resides.

image

We’re not covering the HTML and CSS for this, but this is two asp:HyperLink controls. In the Page_Load method, when retrieving the metadata for the document, the NavigateUrl for these links are set like this:

Close.NavigateUrl = d["CloseUrl"];
Edit.NavigateUrl = d["HostEditUrl"];
Edit.Enabled = !bool.Parse(d["WebEditingDisabled"]);

First I retrieve the CloseUrl from the document metadata and pass into the Close Hyperlink control, next I take the HostEditUrl and configure the Edit Hyperlink control. The URL in the HostEditUrl will contain the URL/urlsrc that is specified as the edit action in the discovery document. Finally I enable the Edit link if the WebEditingDisabled is set to false.

imageIf we now take a look at the UI in SharePoint we have enabled a couple of ways to edit the document; from the viewer and also from the Document fly-out in the document libraries. Just as the WAC Server does with Office documents.

Building the Editor

Creating the editor is very much the same as the viewer. I created a Web Form called editor.aspx in the web project and copied the HTML from the viewer to the new editor.

I modified the menu in the editor.aspx so that it has View and Save buttons instead of the Edit button. The View button takes the user to the WOPI Client viewer and the Save button saves the document back to the WOPI Server (SharePoint in this case). The Save button is an asp:LinkButton so that I can have a server side callback for that method.

The Page_Load method in the editor should look like below. It is very similar to the viewer with a few exceptions:

string _src;
protected void Page_Load(object sender, EventArgs e)
{
    _src = Request.QueryString["WOPISrc"];

    if (!String.IsNullOrEmpty(_src))
    {
        access_token.Value = Request.Form["access_token"];
        access_token_ttl.Value = Request.Form["access_token_ttl"];

        if (!this.IsPostBack)
        {
            // Get the metadata
            string url = String.Format("{0}", _src);
            using (WOPIWebClient client = new WOPIWebClient(url, access_token.Value, access_token_ttl.Value))
            {
                string data = client.DownloadString(url);
                JavaScriptSerializer jss = new JavaScriptSerializer();
                var d = jss.Deserialize<Dictionary<string, string>>(data);
                hlSite.NavigateUrl = d["BreadcrumbFolderUrl"];
                hlSite.Text = d["BreadcrumbFolderName"];
                lblDocument.Text = d["BaseFileName"];
                Close.NavigateUrl = d["CloseUrl"];
                View.NavigateUrl = d["HostViewUrl"];
                Save.Enabled = (bool.Parse(d["SupportsUpdate"]) && !bool.Parse(d["ReadOnly"]));
            }

            // Get the content
            url = String.Format("{0}/contents", _src);
            using (WOPIWebClient client = new WOPIWebClient(url, access_token.Value, access_token_ttl.Value))
            {
                string data = client.DownloadString(url);
                textarea.InnerText = data;
            }
        }
    }
}

I’ve made a couple of changes compared to the viewer. First of all the access_token and access_token_ttl are now hidden input elements on the page instead of local strings, since we need to preserve those values between postbacks because they are needed on all requests back to the WOPI Server. When reading the metadata for the document I set the View button to get the HostViewUrl which is the WOPI Client view URL. Then finally I only enable the Save button if the document supports updates and is not read only. All this is only done if it’s not a postback.

Instead of using the nice syntax highlighter for presenting the document we’re instead using a textarea control, in which we add the contents of the document.

Saving the document

To save the document we need an event handler on the Save button, like this:

protected void Save_Click(object sender, EventArgs e)
{            
    string url = String.Format("{0}/contents", _src);
    using (WOPIWebClient client = new WOPIWebClient(url, access_token.Value, access_token_ttl.Value))
    {
        client.Headers.Add("X-WOPI-Override", "PUT");
        try
        {
            client.UploadString(url, textarea.InnerText);
        }
        catch (WebException we) {
            HttpWebResponse response = we.Response as HttpWebResponse;
            ErrorPanel.Visible = true;
            switch (response.StatusCode)
            {
                case HttpStatusCode.NotFound:
                    ErrorPanel.Controls.Add(new LiteralControl("File unknown/user unauthorized"));
                    break;
                case HttpStatusCode.Unauthorized:
                    ErrorPanel.Controls.Add(new LiteralControl("Invalid token"));
                    break;
                case HttpStatusCode.Conflict:
                    ErrorPanel.Controls.Add(new LiteralControl("Lock mismatch"));
                    break;
                default:
                    ErrorPanel.Controls.Add(new LiteralControl("Unknown error"));
                    break;
                            
            }
        }                
    }
}

The code is pretty straight forward and most of it is exception handling. First of all the communication back to the WOPI Server are done on the same URL as when retrieving the document contents, with the difference that we’re using the HTTP POST method (UploadString method). To be able to update a document you must set the X-WOPI-Override header to “PUT”. If you don’t do this you will get a 404 error.

As you can see I added error handling here as well. To the editor page I added an asp:Panel control which can be used to show any errors. For instance we need to handle cases when someone checked-out the file and set it to read only mode etc. For full information on all possible errors see the [MS-WOP] 3.3.5.3.2 specification.

That was it, all coding should be done by now. Of course you could pimp the user interface with some nice “Saving..” animated gifs etc.

Taking it for a test drive…

If everything compiles fine it’s time to test the editor. Upload a .cs file to any document library and click on it to get to the viewer. From the viewer you should then be able to click on the Edit button and get to the editor. In the editor modify your file and click Save. Verify that your changes is preserved by clicking on the View button or Close the editor and go back to the viewer from the document library. The editor should look something like this:

image

Summary

Now you’ve seen how easy we can build the editor, using the helper classes that we produced in part 2. From here on you should be able to build your own viewers and editors. Make sure that you read the [MS-WOPI] protocol specification so that you now how to handle all the different situations with locked files etc.