Silverlight Deep Zoom and Office Add-ins

I had some ‘free’ time today waiting to give a demo at an MVP conference session – the session over-ran, and I found myself sitting in the hallway for an hour. So I got to thinking about Silverlight and Office. If we assume that Silverlight is more or less a subset of WPF, then it makes little sense to build any Silverlight into an Office add-in: you’ve already got the regular CLR loaded, so why would you want to load a second CLR with a subset of the capabilities? Plus, there’s the performance consideration – Silverlight apps are browser-hosted, so communicating between the SL app and the rest of your solution incurs a perf penalty.

Well, I thought of a couple of possible arguments…

If you can do everything you need to do with Silverlight instead of WPF, then you might actually save some working set – since WPF is bigger than SL. Think about it: if you’re not using WPF, then you don’t have the WPF assemblies loaded. Silverlight’s coreclr + agcore + npctl = approx 2.5Mb. On the other hand, WPF’s PresentationCore + PresentationFramework + WindowsBase + WindowsFormsIntegration = approx 3.7Mb. Not a terribly convincing argument, but still.

Then there are some features that Silverlight supports that WPF doesn’t. Deep Zoom, for example. It might be difficult to imagine a use for Deep Zoom in the context of an Office add-in – but not impossible. Excel, for example, has a range of data visualization capabilities (charting, conditional formatting, heat-maps), but wouldn’t it be nice to use some Deep Zoom-like feature for pivot table drill-downs for instance? There are other features in Silverlight (and not currently supported in WPF) that might be more obviously useful in Office (specifically, the DataGrid control and the VisualStateManager), but remember this was a hallway exercise – and I like Deep Zoom.

So, I built a little proof-of-concept managed Office add-in with a custom task pane that hosts a WebBrowser control, which in turn hosts a Silverlight app. I used the Deep Zoom Composer (available for free download here) to generate the image pyramid. The Deep Zoom Composer is a very nice tool that saves an awful lot of effort in building image pyramids. It also has a very handy ‘Export to Silverlight project’ feature, which creates an entire Silverlight app solution complete with the Silverlight project itself plus an ASP.NET web app (in case you want to host the app on a server). It really couldn’t be any easier – all I had to do was build the Silverlight app – the tool even gave me a ‘TestPage.html’ which hosts the Silverlight app, so that I can test it in a browser.

Deep Zoom is documented here and here, and the Expression Blend team (that owns Deep Zoom) has a blog here. Note that the current version of the Composer tool has a maximum document size of 4 billion pixels – that’s the ‘document’ that includes all the layers of all the images in your final image set. You can easily hit this limit with just 3 or 4 images (one per layer) from a compact digital camera, depending on how much you zoom between layers. I haven’t attached a solution to this post because the image set adds up to about 30Mb.

I created a simple custom UserControl (called WebHostControl) with a fully-docked Windows Forms WebBrowser control as its only child. In my add-in class, I initialized this WebBrowser to load the TestPage.html generated by the Deep Zoom Composer. In accordance with Office UI guidelines, I set up a Ribbon customization with a ToggleButton to toggle the visibility of the task pane.

private WebHostControl whc;

private string silverlightHtml;

private CustomTaskPane slTaskPane;

private void ThisAddIn_Startup(object sender, System.EventArgs e)

{

    silverlightHtml = Path.Combine(

        AppDomain.CurrentDomain.BaseDirectory, "TestPage.html");

}

internal void ToggleSLTaskPane(bool isVisible)

{

    if (isVisible)

    {

        if (whc == null)

        {

            whc = new WebHostControl();

        }

        if (slTaskPane == null)

        {

            slTaskPane = CustomTaskPanes.Add(whc, "Silverlight Pane");

            slTaskPane.Width = 300;

        }

        whc.webBrowser.Navigate(silverlightHtml);

    }

    slTaskPane.Visible = isVisible;

}

This gets a Silverlight app running in an Office add-in. That’s a start, but it’s obviously more useful if the add-in code can communicate with the Silverlight code. Superficially, you’d think this would be easy – after all, they’re both managed components in the same process, and one is loaded (through a couple of layers) by the other. Of course, it’s not so simple – don’t forget that even though they’re both in the same process, they’re running on 2 different CLRs. For my simple PoC, I wanted the user to be able to enter a value in a cell, and have that value be used as the zoom factor in a call to zoom the image in my Silverlight app in the task pane.

Fortunately, Silverlight provides an ‘Html Bridge’ capability, documented here, which allows you to integrate script and managed code in a Silverlight app, including the ability to invoke managed code from javascript on the HTML page. So, I can communicate between the add-in code and the Silverlight code via the HTML script. To use this feature, I first changed the modifier on the Zoom method in the Page.cs in my Silverlight project, to make it public. This method is normally invoked via the mouse handlers (which is obviously a more sensible way to manipulate the zoom than using cell values – but remember this was a science experiment conducted in a hallway (without a chair, I might add)).

public void Zoom(double newzoom, Point p)

{

    if (newzoom < 0.5)

        newzoom = 0.5;

    msi.ZoomAboutLogicalPoint(newzoom / zoom, p.X, p.Y);

    zoom = newzoom;

}

Next, I created a managed class with a method that invokes the Zoom method. I exposed this method for scripting via the ScriptableMember attribute:

public class ScriptableManagedType

{

    private Page thisPage;

    public ScriptableManagedType(Page p)

    {

        thisPage = p;

    }

    [ScriptableMember]

    public void ExposedSilverlightMethod(int cellValue)

    {

        thisPage.Zoom(cellValue, new Point(0.5, 0.5));

    }

}

I instantiated this class at the end of the Page constructor, and made it available for scripting using the RegisterScriptableObject method. This method takes an arbitrary string identifier for the scriptable object – in my case I used the string “ScriptableSilverlightObject”:

ScriptableManagedType smt = new ScriptableManagedType(this);

HtmlPage.RegisterScriptableObject("ScriptableSilverlightObject", smt);

On the HTML page, make sure the Silverlight object has an explicit ID:

<object id="SLP" data="data:application/x-silverlight," type="application/x-silverlight-2" width="100%" height="100%">

Then I can add some javascript to the HTML page, to get hold of the Silverlight plug-in and invoke the exposed method on the registered scriptable object. Note that the arbitrary identifier is dynamically accessible in the script from the Content property of the Silverlight plug-in:

function InvokeSilverlightMethod(cellValue)

{

    var slp = document.getElementById("SLP");

    slp.Content.ScriptableSilverlightObject.ExposedSilverlightMethod(
cellValue);

}

Then, in my add-in project, in my custom UserControl code, I can write a method to invoke this script:

public void InvokeScriptMethod(int cellValue)

{

    this.webBrowser.Document.InvokeScript(

        "InvokeSilverlightMethod", new object[] { cellValue });

}

Finally, in my ThisAddIn class, I can sink the SheetChange event and implement it to invoke the method in my UserControl.

private void ThisAddIn_Startup(object sender, System.EventArgs e)

{

    silverlightHtml = Path.Combine(

        AppDomain.CurrentDomain.BaseDirectory, "TestPage.html");

    this.Application.SheetChange +=

        new Excel.AppEvents_SheetChangeEventHandler(

        Application_SheetChange);

}

void Application_SheetChange(object Sh, Excel.Range Target)

{

    int cellValue = Convert.ToInt32(Target.Value2);

    whc.InvokeScriptMethod(cellValue);

}

So, the sequence of operations is:

- The user changes a value in a cell in the worksheet.

- In the SheetChange event handler, I call the InvokeScriptMethod method on the UserControl that’s hosting the WebBrowser control.

- The InvokeScriptMethod invokes my InvokeSilverlightMethod javascript on the HTML page.

- The javascript method InvokeSilverlightMethod gets hold of the Silverlight object and invokes the scriptable method ExposedSilverlightMethod.

- The ExposedSilverlightMethod calls Zoom to zoom the image.

That seems somewhat painful, but right now it’s the only way (apart from some other possibly even more painful methods involving remoting or WCF or somesuch) – and it’s very nice that there is actually a way to do this kind of communication.

My conclusion here is that if you want sophisticated presentation features in your Office add-in, you’re almost certainly better off using WPF than Silverlight. Even the Deep Zoom functionality could be replicated with WPF, albeit with a lot of effort. Of course, for Deep Zoom or any WPF equivalent to be really useful as a data visualization feature in Office, you’d want to be able to compose the images dynamically – perhaps dependent on the data current in the active document/workbook. Also, the real advantage of Deep Zoom is its intelligent use of network bandwidth – which is not likely to be an issue in an Office add-in if your images are based on data from the current workbook. So, you could build a much simpler image zoom feature with WPF, without the smart download capability, using ScaleTransform. That’s a bit more work than I had time for, but this little exercise pleasantly filled up the hour’s wait in the hallway – and provided some food for thought.