Cum Grano Salis

  • Using SetParameters with the Excel Web Services APIs

    One of the new methods I listed in the post about new features is the SetParameters() SOAP call. This allows callers to set multiple parameters at the same time in Excel Services. In this example I have a simple list containing items and sales of those items. I then use a PivotTable to aggregate the sales and get an average.

    Here’s the workbook snippet:

    image

    On the right there’s the raw table and on the right the PivotTable, showing only the average of sales with a page field acting as a parameter of the workbook. The parameter was named ItemFilter and the average of Sales cell was named AverageSales. The following piece of code shows how to set the parameter and get the value back:

    ES.ExcelService client = new ES.ExcelService();

    client.Credentials = System.Net.CredentialCache.DefaultCredentials;

     

    ES.Status[] status;

    string sessionId = client.OpenWorkbook("http://tsdlt2/docs/ParamPivot.xlsx", "", "", out status);

    ES.ParameterInfo[] infos = new ES.ParameterInfo[1];

    ES.ParameterInfo param = new ES.ParameterInfo();

    infos[0] = param;

    param.Name = "ItemFilter";

    param.Values = new String[1];

    param.Values[0] = "TV";

    client.SetParameters(sessionId, infos);

     

    string val = (string)client.GetCellA1(sessionId, "", "AverageSales", true, out status);

     

    Console.WriteLine("Average Sales are: " + val);

     

    As you can see, first we go and set up the parameters objects – we create a parameter array (you can pass in multiple parameters with one call). In our instance, it has a single item in it. We then create the parameter we want to set up (called ItemFilter") and the value we want to put in (via the Value property).

    Finally, we call SetParameters() with the array – when that call comes back, it will have filtered the PivotTable. Here’s the result:

    image

    Note that if you are using the more recent versions of C#, the code can look a little less verbose:

    ES.ExcelService client = new ES.ExcelService();

    client.Credentials = System.Net.CredentialCache.DefaultCredentials;

     

    ES.Status[] status;

    string sessionId = client.OpenWorkbook("http://tsdlt2/docs/ParamPivot.xlsx", "", "", out status);

    ES.ParameterInfo[] infos = new ES.ParameterInfo[]

           { new ES.ParameterInfo() { Name = "ItemFilter", Values = new string[] { "TV" } }};

    client.SetParameters(sessionId, infos);

     

    string val = (string)client.GetCellA1(sessionId, "", "AverageSales", true, out status);

     

    Console.WriteLine("Average Sales are: " + val);

    Instead of instantiating each element in its own line, we do the initialization inline – makes the code look less complicated (though it does essentially the same thing as we did in the first sample).

  • Using the Web Services APIs to open a workbook for editing and set calculation options

    This post is going to be pretty light-weight. It will show how to use the OpenWorkbookForEditing and SetCalculationOptions methods. I won’t create some over-arching example – the skills you have in interacting with sessions in Excel Services are the going to be the same. I will discuss the behavior and what one can expect.

    OpenWorkbookForEditing

    In SharePoint 2007, developers could open workbooks for transient operations – once a workbook was closed, the sessions went away and all changes to the workbook disapeared. with the introduction of the ability of editing workbooks on EWA, the Web Services API follows suit and allows the developer to apply editing to the workbook. The same methods used to change values in a view-session (that’s what we call the SharePoint 2007-type sessions) work in editing and they work the same way as they did before.

    Here’s what an OpenWorkbookForEditing call looks lilke:

    static void Main(string[] args)

    {

        ES.ExcelService client = new ES.ExcelService();

        client.Credentials = System.Net.CredentialCache.DefaultCredentials;

     

        ES.Status[] status;

        string sessionId = client.OpenWorkbookForEditing("http://tsdlt2/docs/bugs.xlsx", "", "", out status);

        client.CalculateWorkbook(sessionId, ES.CalculateType.Recalculate);

        client.CloseWorkbook(sessionId);

    }

    Of course, the first thing you notice is that there’s not much difference between the call to OpenWorkbookForEditing() and OpenWorkbook(). The important part though is the fact that once this function exists, the workbook will be saved, recalculated back into the server. A few things to know about using the WSAPI to edit workbooks:

    1. Calling CloseWorkbook() does not really close the workbook – it just takes the API out of the list of people using the workbook.

    2. The workbook will save regardless to whether or not you call .Save(). Because Excel Services uses an AutoSave mechanism for saving workbooks, any change is actually written to the workbook.

    3. When editing a workbook, you may get an error if the server recycled by the admin or crashed (this is exactly the same as with view sessions). However, when doing editing, re-opening the workbook has a very good chance of having your changes in it (again, because of auto-save).

    4. When opening a workbook via the API, the presence UI in Excel Services (the thing that shows you who is editing the workbook) will show the user under which the API is running:

    image

    In this case, xladmin is the user inside EWA, viewing the file, and shaharp is the user who is running the APIs. The little swirly icon you see will be replaced by something that indicates that  it’s an API user (I believe we chose a Robot for that).

    SetCalculationOptions

    An API that was requested often by user is the SetCalculationOptions API which can change whether or not a workbook calculates automatically. It is especially useful if you have a workbook that is, by default, auto-recalced and you want to disable that feature while calling multiple SetRange/SetCell calls. That way, the workbook needs not calculate each time and developers can get more perf out of it. The call is straightforward:

    client.SetCalculationOptions(sessionId, ES.WorkbookCalculation.Manual);

    This call will change the calculation mode to manual, meaning that when you set values into the workbook, it will not recalculate. You can turn auto-recalc back on by passing in the Automatic enum value.

    I also want to show you the SaveWorkbookCopy() API, but I will show it in one of the future Javascript examples I will post.

  • What’s new in Excel Web Services in SharePoint 2010

    So far I talked about the new extensibility features in Excel Services 2010 – REST and the new JavaScript OM. I thought I would spend some time talking about the existing extensibility mechanism we have – Excel Web Services. With the new release we have added some new functionality to this SOAP interface to the server. Here’s a short list of the added functionality and their descriptions:

    Here’s the list of of methods that have been added with this release:

    New method Description
    GetChartImageUrl This is probably the most frequently requested feature – the ability to get at charts from workbooks via the WSAPI.
    GetPublishedItemNames This returns the names of the elements in the workbook that have been implicitly or explicitly published. Useful for investigating the structure of a workbook.
    GetSheetNames Returns the names and visibility of sheets on the workbook.
    OpenWorkbookForEditing Allows the API to join an editing session.
    SaveWorkbook Forces a save of the workbook when in editing mode.
    SaveWorkbookCopy Works both in Editing mode and in view mode – saves a copy of the workbook in SharePoint. Another feature that has been requested frequently.
    SetCalculationOptions Allows the developer to change the way the workbook calculates – turn auto-calc off and on.
    SetParameters Sets parameter on the workbook. This is especially useful if you wish to set page filters that have been designated as parameters but also when you want to set multiple cells at the same time.

    This first post will show how to use the GetChartImageUrl() call to get an image from Excel Services. I will use the same workbooks we have been using so far, but show how to work with them with the Web Services APIs. Our solution will open the TeamBugs.xlsx file, iterate over the people who are reported on in the workbook and generate a chart for each, saving it to local disk.

    Step 1 – Adding a reference to the Excel Services SOAP APIs + Skeleton code

    In Visual Studio (inside your project – in my case I am using a console project), right-click the project and select “Add Service Reference”. This will bring up the WCF client proxy generator dialog.  In it go to “Advanced” and then “Add Web Service”  - you will be presented with the usual UI for adding SOAP proxies (see my older posts about this).

    Next we add the skeleton code - this code is almost always the same whenever you write Excel Services code – it creates the client and sets it up:In here, we also open up the workbook called teamBugs.xlsx.

    static void Main(string[] args)

    {

        ES.ExcelService client = new ES.ExcelService()

        client.Credentials = System.Net.CredentialCache.DefaultCredentials;

        ES.Status[] status;

        string sessionId = client.OpenWorkbook("http://tsdlt2/docs/teambugs.xlsx", "", "", out status);

    }

    That’s it – we have a session ID – we can start partying with the server.

    Step 2 – Getting the range containing the relevant aliases we want to generate the charts out of

    This is nothing new – I have it here for completeness mostly. The code will grab the range called AliasList:

    object[] rows = client.GetRangeA1(sessionId, "", "AliasList", true, out status);

    foreach (var o in rows)

    {

        object[] row = (object[])o;

        Console.WriteLine("Generating chart for " + row[0]);

    }

    The result (placed in rows) is a jagged array – or an array of arrays. In this case, we get it from the server and then iterate on each element in the top-level array – row. Each item in that array is an array by itself – an array of cells. Since we only care about the first one, this is what we will use.

    Step 3 – Setting the value of the alias into the workbook

    Again – nothing new – by using the SetCellA1 method, we take each of the values we read and set them in the cell called Alias:

    object[] row = (object[])o;

    Console.WriteLine("Generating chart for " + row[0]);

    client.SetCellA1(sessionId, "", "Alias", row[0]);

     

    Step 4 – Getting the chart image

    Finally we can use the new shiny method! GetChartImageUrl() returns a URL to the requested chart. Here’s an example of the call:

    Console.WriteLine("Generating chart for " + row[0]);

    client.SetCellA1(sessionId, "", "Alias", row[0]);

    string url = client.GetChartImageUrl(sessionId, null, "PersonalBugs", out status);

    The first parameter, as always, is the session id. The second parameter is the size of the chart requested – in this case we pass in null which means we want the default size. We could pass an instance of the Size class which contains Width and Height. The third parameter is the name of the chart and the final one is the usual status out parameter.

    What do we now do with the URL then? We read it of course, and write it into disk:

    WebRequest req = WebRequest.Create(url);

    req.Credentials = System.Net.CredentialCache.DefaultCredentials;

    using(FileStream output = File.Create("c:\\temp\\" + row[0] + ".png"))

    using(WebResponse response = req.GetResponse())

    using (Stream input = response.GetResponseStream())

    {

        byte[] buffer = new byte[1024];

        int read;

        while ((read = input.Read(buffer, 0, buffer.Length)) > 0)

        {

            output.Write(buffer, 0, read);

        }

    }

    The process is relatively simple, using .NETs classes.

    We create a WebRequest class based on the charts URL. We then create a new file (in this case, named after the alias and located in the temp folder) and get the response from the web request (that’s the two calls to GetResponse() and GetResponseStream(). Finally, we read the chart stream block by block and write it into the file.

    Here’s what the final program shows in the console:

    image

    And here’s Explorer looking at the temp folder after the program is done running:

    image

    As you can see, we generated a number of charts, each calculated specifically for each of the aliases supplied.

    Here’s one of these charts in full size:

    amiravr

    That’s it! As you can see, it’s fairly simple to get a chart from Excel Services. You can use the stream that comes back in the CLR’s Image class to load the image directly and show it, say, on a Form. Remember that the chart is valid only for as long as the session is valid! Closing the workbook and trying to access the URL will result in failure.

    Next post I will talk a bit about OpenWorkbookForEditing() and SetCalculationOptions().

  • Introduction to the Excel Services JavaScript OM - Part 3 – writing to the workbook

    So far we saw how one can call into the EWA at arbitrary times and respond to events by affecting the page around the EWA. This time around we will combine the two mechanisms to make modifications to the workbook when the selection changes.

    If you will recall, in part 1, we listened to events over the list of aliases in the workbook, giving the dashboard the ability to show the picture of the relevant person. This time around, we will also take the name and “write” it into another cell in the workbook. That in turn will update the EWA and show us the updated result for that person.

    Step 1 – Figure out the range we want to write to

    Working with ranges is an inherently asynchronous operation – before you can actually write to it, the system needs to know where it is. For that, we need to make a call to the getRangeA1Async() method of the workbook. Note that if we wanted to go by coordinates, we could call the getRange() method which is not asynchronous and thus may be more fitting for some scenarios:

    function cellchanged(rangeArgs)

    {

        var sheetName = rangeArgs.getRange().getSheet().getName();

        if (lastSheet == sheetName)

        {

            var col = rangeArgs.getRange().getColumn();

            var row = rangeArgs.getRange().getRow();

            var value = rangeArgs.getFormattedValues()[0][0];

     

            if (sheetName == "Sheet1" && col == 1 && row > 1 && value && value != "") {

                var img = document.getElementById("dashboardPersonPicture");

                img.src = "/PersonalPages/" + encodeURIComponent(value) + ".jpg";

                ewa.getActiveWorkbook().getRangeA1Async("Sheet2!Alias", getRangeComplete, value);

            }

        }

        lastSheet = sheetName;

        writelog('Address:' + rangeArgs.getRange().getAddressA1(), 1);

        writelog('Value:' + rangeArgs.getFormattedValues(), 1);

        writelog('Cell changed event fired', 0);

    }

     

    The highlighted code is what we added over the existing method from the previous post. In this case, we ask for the cell named “Alias” and we tell the EWA to call into getRangeComplete() when it has the object ready. Note that we also pass in value as a parameter – we do that so that we later know what value to put into the cell (we could also have used a global variable for this if we were so inclined).

    On top of that, notice that we added a condition where the code executes only in the case where the sheet name has changed. This is important because the cell-changed event also occurs when a sheet changes. This means that the second the user tries to switch back to the first sheet (where they originally clicked), since the active cell is still the same one, the event will fire, the code will run and immediately move the user again to the other sheet. Checking that lastSheet equals the sheet name that we currently are on makes sure we only run the code for actual navigation within the sheet.

    Step 2 – Write the new value into the cell

    Once the callback gets executed, we need to call into the setValuesAsync() method:

    function getRangeComplete(asyncResult)

    {

        var range = asyncResult.getReturnValue();

        var value = asyncResult.getUserContext();

     

        var values = [[value]];

        range.setValuesAsync(values);

    }

    The first step in the callback of getting the range is to get the values out of the asyncResult parameter – getReturnValue() is the standard method we use to get the result of the operation. getUserContext() is the method we use to get the user state passed as the last parameter to all asynchronous methods.

    We then create an array out of the value ([[value]] creates a jagged array with a single item in it). That is passed into the setValuesAsync() method which will update the EWA. No callback is being passed to the setValuesAsync() method – this is because we don’t need to take further action. If we were to add more error handling, we would have had to use that method.

    Clicking on “shaharp” and flipping to sheet2 (which is where the Alias named range is) will show us that the cell has indeed changed and with it changed the chart:

    image

    Step 3 – Navigating to Sheet2 for the user

    The final step is to make sure the EWA actually shows the relevant information when the user clicks on the cells. For that we will use the range.activateAsync() method. We could have waited for the setValuesAsync()  to complete before making this call, but there’s really no reason – Excel Services can take multiple calls at the same time. This means we simply add the to the getRangeComplete() function:

    function getRangeComplete(asyncResult)

    {

        var range = asyncResult.getReturnValue();

        var value = asyncResult.getUserContext();

     

        var values = [[value]];

        range.setValuesAsync(values);

        range.activateAsync();

    }

     

    And that’s it! Clicking on “danyh” for example, will cause sheet2 to get activated and the “alias” cell to change to “danyh” which in turn will cause the chart to change accordingly:

    image

    And we are done!

    As you can see, the JavaScript OM gives developers the abilities to add more capabilities to their workbooks and code around them. Do not expect an OM as rich as Excel’s, however, the capabilities are nice to start with – I would love to hear from you through the comments what do you think is missing from the OM.

  • Introduction to the Excel Services JavaScript OM - Part 2 – listening to events

    The previous post showed how to start using the Excel Services JavaScript OM to automate the EWA web-part. The example given showed how to cause periodic recalculation on a workbook that’s on a SharePoint web-page.

    In this post, we will add our first event to the EWA and see how we can get an even more integrated experience. The feature we are going to code in this step is to give users the ability to click in our dashboard on a specific person alias in one of the sheets and use that value to show a picture of that person on the side of the web-part page.

    Step 1 – Listening to an event

    But before we can do all this image handling, first thing’s first – we need to listen to events. Hooking up event is typically done in the ewaApplicationReady() method described before – that’s where the EWA is ready and able:

    function ewaApplicationReady()

    {

        ewa = Ewa.EwaControl.getInstances().getItem(0);

        // Start recalcing the workbook every 5 minutes.

        setTimeout(forceRecalc, RECALC_INTERVAL);

        ewa.add_activeCellChanged(cellChanged);

    }

     

     Note the line that was added to the method – ewa.add_activeCellChanged() – in this case we are hooking up to get notification whenever the active cell changes on the workbook. Now, each time a cell is selected, the EWA will call this function. Lets take a look at what the prototype of this function looks like:

    function cellchanged(rangeArgs)

    {

    }

    The rangeArgs parameter in the function will contain information on the range clicked. Specifically, it will contain the following three methods:

    getRange() – This returns the Range object that’s behind the cell that was selected.

    getWorkbook() – The workbook object that is currently loaded in the EWA.

    getFormattedValues() – The value that’s in the cell.

    In our case, we will use the getFormattedValues() to figure out what the text is in the cell and use that to display the image.

    Step 2 – Adding the image tag so that we can display an image

    In the first post, the HTML fragment we are using inside the content-part contains a single <script> tag containing our code. It is obviously quite possible to also have any other HTML in there – in this case, we will add an IMG tag:

    <img id="dashboardPersonPicture" />

     

    Step 3 – Setting the image due to cell change

    We will modify the cellchanged() function in the following manner:

    function cellchanged(rangeArgs)

    {

        var img = document.getElementById("dashboardPersonPicture");

        img.src = "/PersonalPages/" + encodeURIComponent(rangeArgs.getFormattedValues()) + ".jpg";

    }

    What this does is take the formatted value of the cell that was selected and uses it in a URL that is set into the img tag – in this case, the URL will be /personalpages/shaharp.jpg when my alias is picked. And here’s how this looks in the browser:

    image

    That’s great! But watch what happens when a different random cell is selected (say, the one that says “Alias”):

    image

    The problem is that we are indiscriminately setting the image. Instead, we need to make sure that the range where the user clicked makes sense.

    Step 4 – Looking at the Range object

    To do this, we will take the Range object that comes back as part of the rangeArgs parameter and use it to check the coordinates of the cell that was selected. If it falls under our scenario (the column in question), only then will we actually set the image:

    function cellchanged(rangeArgs)

    {

        var sheetName = rangeArgs.getRange().getSheet().getName();

        var col = rangeArgs.getRange().getColumn();

        var row = rangeArgs.getRange().getRow();

        var value = rangeArgs.getFormattedValues();

        if (sheetName == "Sheet1" && col == 1 && row > 1 && value && value != "")

        {

            var img = document.getElementById("dashboardPersonPicture");

            img.src = "/PersonalPages/" + encodeURIComponent(value) + ".jpg";

        }

    }

    The code uses the getSheet().getName(), getColumn() and getRow() calls on the range to figure out exactly where the selection is, and if it falls under the range we are interested in, only then will we go and set the URL for the image.

    And that’s it for this post – we saw how to hook up events and then we saw how to use the argument in the event to figure out where the user clicked in the document and what the content of the selection is. Next up, we will see how we can write back values into the workbook.

  • Introduction to the Excel Services JavaScript OM - Part 1 – Humble beginnings

    In the past few weeks, I wrote a bunch of posts about the new REST APIs in Excel Services. I wanted to take a bit of a breather from those APIs and start writing about the new JavaScript browser-side OM we are shipping with SharePoint 2010. This OM will allow developers to customize, automate and otherwise drive the EWA control and build more compelling, integrated solutions.

    In the next few posts, I will show how developers can use the new OM to do all kinds of things to the EWA.

    Step 1 – Creating the page

    To begin though, we will need an EWA and a content-part. The example will revolve around a fictitious dashboard for my teams bugs – each post will add more features to the dashboard. For this, we will create a new Web-part page in our site – from the “Site actions” drop down, choose “More Options” (Note – all these steps are very similar to what you would do in SharePoint 2007 – I am just giving them for completeness):

    image

    Once chosen, you will be presented with a host of options of what to create – select web-part page:

    image

    After that, you can choose whatever layout you want – in this case we will select the “Header, Left Column, Body” and call it “TeamBugsDashboard”. Once it’s created, we can easily add an EWA to the page by clicking on the right column “Add a Web Part” area:

    image

    Selecting the Excel Web Access web part and then hitting add will place it on the web page. We will do the same with a web-part called “Content Editor” under the “Media and Content” category – this time adding it to the left column. The result should look similar to the following (the left circled area is the content element and the right one is the Excel Web Access web-part):

    image

    Next, we will select the workbook we want displayed in the EWA – to do that, we will use the dropdown that’s on the header of the control and choose “Edit Web Part”. This will present with a pane on the right where we can put the name of the workbook – in there we can type the URL to the workbook in the “workbook field”.

    image

    Hitting the “OK” button will allow the EWA to show the relevant workbook. This is where things become a little interesting – the content web-part will contain a reference to an HTML fragment that’s contained in another document library (we will show the actual content of the HTML fragment shortly):

    image

    This tells the content web part to get the file linked and embed it as part of the web-page.

    Step 2 – adding the JavaScript OM skeleton code

    Now, to start working with the JavaScript OM, there’s some standard code that needs to run – the following is the skeleton piece of code you need to add to your page when you want to interact with the EWA:

    <script type="text/javascript">

        ///<reference path="http://tsdlt2/_layouts/EwaJsom.ashx?v=1"/>

        var ewa = null;

     

        //set page event handlers for onload and unload

        if (window.attachEvent)

        {

            window.attachEvent("onload", ewaOmPageLoad);

        }

        else

        {

            window.addEventListener("DOMContentLoaded", Page_Load, false);

        }

     

        //load map   

        function ewaOmPageLoad()

        {

            if (typeof (Ewa) != "undefined")

            {

                Ewa.EwaControl.add_applicationReady(ewaApplicationReady);

            }

            else

            {

                // Error - the EWA JS is not loaded.

            }

        }

     

        function ewaApplicationReady()

        {

            ewa = Ewa.EwaControl.getInstances().getItem(0);

        }

    </script>

     

    That’s pretty much it for the skeleton or bootstrapping code – you can pretty much copy&paste this code whenever you want to interact with an EWA on a page. In a future post I will show how to find a specific EWA control (in the case where you have more than one). The code is relatively simple – the first block in the script will execute and add an event handler to the page so that we know when it loads (the reason we use the attachEvent/addEventListener mechanism is so that we do not override the page load event). Then, when the ewaOmPageLoad() method is called, we make sure that the OM is loaded (that’s the call to typeof(Ewa) != undefined and if it is defined, we go ahead and add another callback to an event – this time for the applicationReady event on the EWA – this is done by calling the add_applicationReady() method. This will fire once the EWA itself is up and running. When that happens, the code executing in ewaApplicationReady will store the EWA control reference in a variable so we can start using it.

    Step 3 – Making the EWA recalc periodically

    Now is when we start adding actual functionality. To begin, we will address one request dashboard writers sometimes express – the means to periodically recalculate the dashboard so that it shows up-to-date information w/o requiring user interaction. To do that, we will call into an OM method called recalcAsync(). Lets start simple – here’s what the ewaApplicationReady() function looks like after the call to recalcAsync:

        function ewaApplicationReady()

        {

            ewa = Ewa.EwaControl.getInstances().getItem(0);

            // Start recalcing the workbook every 5 minutes.

            setTimeout(forceRecalc, RECALC_INTERVAL);

        }

     

    The setTimeout() call will ensure that we do not immediately start recalculating, but instead wait for however we decide we want between. Next, this is what happens in the forceRecalc() function:

    function forceRecalc()

    {

        ewa.getActiveWorkbook().recalcAsync(recalcComplete, null);

    }

     

    Again – a fairly simple call – we use the ewa variable to get the currently active workbook (getActiveWorkbook()) and call into the recalcAsync() method. Now, since most operations that actually have to do with the workbook that resides on Excel Services need to get to the server and back, almost all of those are actually asynchronous methods. As part of the call, you pass in the callback (recalcComplete() in our case) and an opaque user state object (null – we don’t need it). Once the recalculation is done, the EWA will call us back. Note – in some cases, you do not really care when the call comes back – in these, you can simply pass in null (or omit the parameter) and treat this call as a “fire and forget” call. The reason we care about when the call is done in this case is because we want to make sure we queue up another calculation request when the first one is done. Here’s the recalcComplete() callback:

    function recalcComplete(result)

    {

        setTimeout(forceRecalc, RECALC_INTERVAL);

    }

     

    Notice that we actually ignore the result in this case – we just make sure we issue another call to the recalcAsync() method. Since this will continue to happen over and over again, we will see the EWA recalc every RECALC_INTERVAL milliseconds.

    By the way: The reason we are using setTimeout() and not setInterval() is so that in the case where the server is swamped, we do not bombard it with calls to recalc – this way we guarantee that we only tell the EWA to recalculate after it has actually finished the last time we asked it to recalculate.

    That’s it for this post – you now know how to start working with the new JavasScript OM Excel Services is exposing – in the next few posts, we will explore some of the other functionality exposed and how it is used.

  • Creative workarounds – Windings edition

    11/24/09 - Correction - Sergei contacted me to explain that I got it all wrong - Windings does not give all the required shapes (Circle, Triangle and Square).Instead, Sergei had to hunt until he found the perfect font that supported all that perfectly - Good ol' "Arial Unicode MS". 

    In older posts, I have talked about a developer working on internal Microsoft application who really gives Excel Services a run for its money. Sergei has “starred” in two other posts and after playing for a bit with REST, has come up with a really nice workaround to the limitation of the REST API that does not allow for icon conditional formatting. Sergei instead creatively uses Windings and formatting conditional formatting to convey the same idea. Here’s one of his score cards inside a gadget, minimized:

    image

    As you can see, there’s a yellow triangle showing that whatever-it-is we are watching is not doing too badly or too well. Here’s the score-card maximized in the gadget:

    image

    This is similar to the Excel mechanism that shows icons for conditional formatting:

    image

    I love crazy people!!

  • Excel Services REST limitations

    We just went through two or three weeks worth of posts and I tried to show you some of the new stuff we can do with Excel Services. It’s cool, it’s exciting, it’s great. But here comes the caveat post!!

    There are a number of things that do not currently work in REST – the following is a partial list, but it contains the more painful missing parts:

    1. No floating charts – If a range contains a chart on it and you request the range via REST, you will get just the range – where the chart is, you will actually see the stuff that’s under it.

    2. No sparklines, no icon conditional formatting – due to resource constraints, we do not do either of these. We know it’s a shame, and we want it to work too, but it’s just going to have to be one of those things we add later on.

    3. Not pixel-perfect with EWA – even though the HTML that REST produces is very close to the HTML the EWA produces, it’s not perfect. The fact that we do not have access to all the CSS elements that EWA has is part of it (remember, we return an HTML snippet – it needs to be self contained as much as possible).

    4. No distinction in tables (when requested as ATOM) about whether or not something is a column head or a total.

    5. URL size is limited to around 2000 characters. That means that if you have a large number of parameters, you may not be able to set all of them (especially if the workbook itself is pretty deep in folders).

    There are probably a number of other limitations and caveats – I will add them here as you guys complain about them.

  • Coding the Excel Services Windows 7 Gadget – Part 5 – Next steps

    There are a few things I want to change/fix and a few features I want to add to the gadget. Here they are, in no particular order:

    Changes:
    1. I want to change the way the gadget works to be more like the fly-out – instead of relying on the settings to tell the gadget how to display the content, I want to sniff it and display it the correct way like that.
    2. Need to go over the code – I think there are cases where errors are not properly handled. I also think that there are cases where there are circular references which may cause issues with memory.

    New features:
    1. I want to add links at the top of the fly-out, to allow people to actually open a browser to the content.
    2. Add more link types that can be in the fly-out – maybe detect any image and allow it to be displayed in the fly-out, detect youtube links and embed the movie in the fly-out etc.
    3. Allow users to have “macros” for links. I can imagine having http://$server$/[…] resolving to the server the workbook comes from. Similarly the file and maybe the document library.
    4. Have a mechanism in the settings window that allows you to enter arbitrary links instead of a rigid selection mechanism.
    5. Use  the new SharePoint list APIs to show what files are available (so users don’t have to know the full URL by heart).

    If you have other ideas, do  not hesitate to suggest them in comments!

  • Coding the Excel Services Windows 7 Gadget – Part 4 - Fly-outs

    One of the features gadgets posses is the ability to have a “fly-out” visual aid for “zooming in” or “drilling down” on parts of the gadget. The Excel Services gadget supports that too – for certain types of links. Generally speaking, one should not muck around with the HTML that comes back from Excel Services. If you do decide to do that, be aware that your solution may stop working because Microsoft does not guarantee the structure of the HTML. That said, lets see how we can muck around with the HTML. :)

    As a reminder, this is what a Flyout looks like in Excel Services:

    image

    The right part is the gadget and clicking on the left column in the gadget brings up the fly-out. The normal behavior in a gadget – when a link is clicked – is to bring up a browser with that link. What we want is that for certain cases, the data will show up in a flyout. To achieve that, the HTML that wraps the content of the gadget has an onclick event associated with it. This event is fired whenever somebody clicks inside the content – and that includes clicking on links. Here’s what the code for the event looks like:

    // Brings up the flyout for links. This is attached to the OnClick event that wraps

    // the content.

    function showFlyoutForLinks()

    {

        var hideFlyout = true;

       

        // Check to see that  it's an anchor that was clicked..

        if (event.srcElement.tagName == "A")

        {

            if (event.srcElement.href) // then make sure it has a link.

            {

                if (event.srcElement.href.toUpperCase().indexOf("_VTI_BIN/EXCELREST.ASPX") != -1) // Then, make sure it's a link to a REST URL.

                {

                    // If all these things are true, that means we need to show a flyout with the data.

                    System.Gadget.Flyout.file = "flyout.html";

                    System.Gadget.Flyout.show = true;

                   

                    // Tell the flyout what URL it needs to go to.

                    System.Gadget.Flyout.document.restUrlNavigation = event.srcElement.href;

                   

                    // setting event.returnValue to false will make sure the link does not actually invoke.

                    event.returnValue = false;

                    hideFlyout = false;

                }

            }

        }

     

     

        if (hideFlyout && System.Gadget.Flyout.show)

        {

            System.Gadget.Flyout.show = false;

        }

     

    }

     

    First thing we do when processing a click, is to check to see what element got clicked – by checking the tagName property, we make sure the element is an anchor (a link). If it also has an HREF, that means we can process and check to see if we need to bring up the fly-out.

    The next condition checks to see if the string “_vti_bin/ExcelRest.aspx” is part of the URL – if it is, we assume that that’s a REST URL and we will use the fly-out to display the information.

    We tell the gadget to show the fly-out and use the flyout.html file to do so. We set an expando property on the document so that the fly-out will know what URL it needs to load as well. Finally calling event.returnValue = false guarantees that the gadget won’t continue processing the click event (and thus won’t execute the standard link behavior of opening the link).

    The HTML in the flyout.html file contains the following body:

    <body onload="setTimeout('flyoutLoadRestUrl()', 0)" style="width:40px;height:40px;">

    When the fly-out completes loading, the flyoutLoadRestUrl() will execute:

    // Attached to the load event of the body of the flyout. Starts the whole loading of the flyout.

    function flyoutLoadRestUrl()

    {

        loadRange(document.restUrlNavigation, gebid("flyout"), 0, flyoutElementComplete);

    }

     

    This uses the same exact method the gadget itself uses to load ranges – but it gives a different callback – flyoutElementComplete(). This method is somewhat interesting because of some of the constraints we have. Namely, we do not know what the type of the URL is – it may be a chart and it may be a range. Since I did not want to parse the URL to figure that out, I had to use a different mechanism:

    // This is used as the callback for loadRange() when it appears in the flyout.

    function flyoutElementComplete(element)

    {

        var targetParent = element.targetParent;

     

        // With the flyout, we dont necesserily know what to do with a URL - if it's a Range, we want

        // to embed the stream that comes back. If it's an image, we want to put it in an <img> element.

        // What we do here, is detect what the data is by looking at the Content-Type http header.

        var contentType = this.getResponseHeader("Content-Type").toUpperCase();

        if (contentType.substring(0, 5) == "IMAGE")

        {

            // For images, the content type will be image/png - we detect that and make sure we add an Image tag to the

            // flyout.

            element = document.createElement("img");

            element.src = this.requestUrl;

           

            // We set up the event to fire when the image is done loading.

            element.onload = flyoutImageLoaded;

            targetParent.appendChild(element);

        }

        else // It was a range, not an image.

        {

            // Append the element to the target (inside the flyout).

            targetParent.appendChild(element);

           

            // Adjust the flyout size.

            flyoutAdjustContent();

        }

       

    }

    This callback is somewhat more involved. First, we check to see what the content type that got returned is (this is returned in the Content-Type HTTP response header). If the element is an image, we know we need to use a slightly different mechanism – put in an IMG tag – we also need to make sure that when the image loads, we adjust the size, so for that we have the flyoutImageLoaded callback attached to the onload event.

    If, on the other hand, the content type that got returned is not an image, we will go and set the content of the fly-out to contain the returned range.

    So that’s it pretty much for coding! Here’s what we saw in the past four posts:

    1. The settings window of the gadget uses the REST discovery mechanism to figure out what elements exist in a workbook.
    2. The gadget window uses REST calls to get charts and adjust it’s size.
    3. The gadget window uses REST to get Ranges and embed them inside the gadget.
    4. Override the way links work to show a fly-out for the gadget when a REST link is clicked.

    There’s just one more post in this series – discussing some of the changes I want to make to the gadget and some of the new features. After that, we will take a break from REST and talk about some of the other new programmability capabilities Excel Services has with the new version.

  • Coding the Excel Services Windows 7 Gadget – Part 3 - Ranges

    The first post in this series talked about how to code the settings window and the second talked about how the chart contents is displayed on the gadget itself. In this post, I will show how ranges are treated when the user requests to see them on the gadget.

    Ranges (and tables and PivotTables) are different from charts because there’s no convenient mechanism that will load HTML fragments into a page. Instead, we need to use XmlHttpRequest to load the HTML and embed it into the gadget itself.

    When I showed the adjustContent(), there was a call to the loadRange() in it:

    else if (tnt == config.typeRange || tnt == config.typeTable || tnt == config.typePivotTable)

    {

        if (!loaded)

        {

            loadRange(config.get_resolvedThumbnailUrl(), gebid("thumbnailContainerDiv"),0);

        }

    }

     

    The first parameter we pass in is the URL to the content we want. The second parameter is the element which will contain the fetched REST HTML fragment and the last is the index of the loaded item (so we can track if it was loaded successfully or not). Here’s the content of that method:

    // Loads a range from REST and puts it in a DIV. Since this an asynchronous operation, there are a number of stages that take place here:

    // 1. A hidden DIV is created "off screen" and an async called made to the appropriate URL.

    // 2. When the call is done, the result is taken from the DIV and placed in the actual location (thumbnail or full-sized).

    // 3. Adjust content is called.

    // Note that when used in the flyout, this call also knows how to load images (the callback detects the image and handles it properly)

    function loadRange(url, targetElement, loadedIndex, completeCallback)

    {

        if (!completeCallback)

        {

            completeCallback = loadedElementComplete;

        }

     

        debugWrite("Loading small range");

       

        // Create the "off screen" DIV.

        var rangeLoader = document.createElement("div");

        // Create an expando property to hold the loaded index - the item we are trying to load (minimized/maximized)

        rangeLoader.loadedIndex = loadedIndex;

        // Append the element to the hiddent div.

        gebid("loadContainer").appendChild(rangeLoader);

       

        // Create the appropriate range request.

        var rangeRequest = new XMLHttpRequest();

        // Store the xmlHttpRequest on the off screen DIV.

        rangeLoader.xmlHttpRequest = rangeRequest;

        // Set properties/events

        rangeRequest.onreadystatechange = rangeRequestReadyStateChanged;

        rangeRequest.open("GET", url);

        // Also create a reference back from the XML Http Request into the DIV.

        rangeRequest.divTarget = rangeLoader;

        rangeRequest.loadedElementComplete = completeCallback;

        rangeRequest.requestUrl = url;

     

        // Set the targetParent on the off-screen DIV to be the element we actually want it to go into eventually.   

        rangeLoader.targetParent = targetElement;

     

        // Send the async request.

        rangeRequest.send();

    }

    As you can see from the function declaration, it actually knows how to take a fourth parameter. This will be used in a future post – for now, omitting the completeCallback parameter will default the value to the loadedElementComplete function (this was shown in the previous post).

    Here’s what we do to fetch a range – just like before, we create an element which we add to the hidden DIV – this element will be used as a temporary store for the loaded data. We set up the newly created DIV and add it. We then create an XMLHttpRequest() object which we will point to the URL that will return the HTML fragment. We set up the callback (via the onreadystatechange property) and we also give a second callback (via the expando property loadedElementComplete) which is where the actual interesting stuff is going to occur. Finally, we send the the HTTP request.

    The rangeRequestReadystateChanged() has one important job – it takes the result that comes back from the HTTP call and inserts it into the target DIV:

    // Used by loadRange to figure out when a request is done.

    function rangeRequestReadyStateChanged()

    {

        if (this.readyState == 4)

        {

            if (this.status == 200)

            {

                this.divTarget.innerHTML = this.responseText;

                this.loadedElementComplete(this.divTarget);

            }

            else

            {

            }

        }

    }

    The callback makes sure that the request is really complete, in which case it sets the inner HTML of the target DIV to be the resulting text and it then calls the callback that was set on it (that’s the expando completeCallback property discussed above).

    And that’s it – from loadedElementComplete() we will make a call to adjustContent() that will resize the gadget and zoom out if needed (in the case of a minimized gadget).

    With this post, we covered all the basic elements of the gadget. The next post will show how the flyout works and the one after that will discuss improvements and the like for future versions of the gadget.

  • Coding the Excel Services Windows 7 Gadget – Part 2 - Charts

    The first part of this series showed how the code in the Settings form worked in the gadget. In this part, I will start describing how the gadget fetches the requested information from Excel Services and displays it.

    As a reminder, here’s what the gadget looks like when minimized and showing a chart:

    image

    The gadget reloads itself in two cases – when the refresh interval has passed and when the settings window is OKed. In both cases, the call that gets made is to the method adjustContent which takes care both of shooting off the loading of the data and adjusting to the size of the content. The function has different processing for when the gadget is maximized and when it’s minimized which are pretty similar. Here is the function with the handling of the maximized gadget removed for brevity (since this method is relatively long, I am also adding comments with stages in them so I can refer to them):

    // This method adjusts the content of the gadget to fit. It also takes care of refreshing the data if necessery.

    // If the content is not yet loaded, Adjust will make a call to the loading code and exit. Once the content is loaded,

    // adjust will be called AGAIN and this time adjust for the actual loaded content.

    function adjustContent()

    {

        // [[[ STAGE 1 ]]]

        showLoading(false);

        var initDocking = true;

       

        if (isDocked()) // When the gadget is docked (minimized/thumbnail) this code will run.

        {

            debugWrite("Adjusting for small");
            // [[[ STAGE 2 ]]]

            var loaded = loadedArray[displayThumbnail];

            var tnt = config.get_resolvedThumbnailType();

           

            // If needed, try to load the data - depending on what we are actually showing.
            // [[[ STAGE 3 ]]]

            if (tnt == config.typeChart)

            {

                if (!loaded)

                {

                    loadSmallChart();

                }

            } // [[[ STAGE 4 ]]]

            else if (tnt == config.typeRange || tnt == config.typeTable || tnt == config.typePivotTable)

            {

                if (!loaded)

                {

                    loadRange(config.get_resolvedThumbnailUrl(), gebid("thumbnailContainerDiv"),0);

                }

            }

            else

            {

                initDocking = false;

            }

     
            // [[[ STAGE 5 ]]]

            // If we are loaded, adjust the size and content to properly show the data.

            if (loaded || !initDocking)

            {

                // Figure out the zoom - this is done by taking the content size and making sure it fits in 120pixels width.

                gebid("thumbnailContainerDiv").style.zoom = 1;

                gebid("undocked").style.position = "absolute";

                var ratio = 120 / gebid("thumbnailContainerDiv").scrollWidth;

                if (ratio >= 0.95)

                {

                    ratio = 1;

                }

     

                gebid("thumbnailContainerDiv").style.zoom = ratio;

                document.body.style.height = gebid("docked").scrollHeight;

                gebid("undocked").style.position = "static";

            }

            else // [[[ STAGE 6 ]]]

            {

                // Show the little circle that denotes a loading gadget (the thing that goes round and round).

                showLoading(true);

            }

            document.body.style.width = "120px";

            debugWrite("BodyW:" + document.body.style.width);

        }

        else // !isDocked - If the gadget is maximized, we need to adjust it appropriately.

        {

            // [[[ Code removed from here ]]]

        }

     

        if (initDocking)

        {

            setupDocking();

        }

     

     

     

    Here’s what’s happening in this method:

    Stage 1:
    We make sure the loading indicator (the little circly thing that goes round and round when loading) is hidden and we check to see if the gadget is docked or not (remember, a docked gadget is “minimized” or small and an undocked gadget is “maximized” or big). The processing shown here is just for handling of the minimized gadget.

    Stage 2:
    We have a global state that tells us whether or not the content of the gadget has been loaded – that’s stored in the variable called loadedArray[] – the first entry (displayThumbnail) is for the minimized gadget. If the content has not been loaded (or has expired), adjustContent will fire up an update – this can be seen in stages 3 and 4. The call to config.get_resolvedThumbnailType() returns the type of the content to be displayed – this can be a range, a chart, a table or a PivotTable.

    Stage 3:
    In the case of a chart and in the case that the gadget still has not loaded, we will call into the loadSmallChart() code (we will look into that a little later). Note that the call to config.get_resolvedThumbnailUrl() will return the URL to the content the user wants to see when the gadget is minimized.

    Stage 4:
    In the case of Ranges, Tables and PivotTables, the gadget will call into a different method called loadRange(). The parameters passed comprise of the URL to load, the element into which the range needs to be placed (the details of how ranges are loaded will be shown in the next post in this series).

    Stage 5:
    In here, if the gadget has been loaded (which it won’t be the first time you go into adjustContent()) the code will adjust the size of the minimized gadget to show the data. The code takes the size of the containing div (called thumbnailContainerDiv) and checks to see its size – if the size is too large for the minimized gadget (the width needs to be 120 pixels), it will zoom that specific div so that the HTML will fit.

    Stage 6:
    If the gadget still has not loaded it’s data, the adjustContent() method will show the loading indicator.

    The code that was removed (handling the maximized gadget) is similar – the major difference is that instead of adjusting the content to fit in 120 pixels, the gadget will adjust it’s size to fit the content.

    Before we delve into the loadSmallChart() method, it’s important to understand how the gadget update occurs. When updating, the gadget uses a hidden DIV on the HTML (called loadContainer) to anchor the new content. The reason for this is so that if the content fails to load for whatever reason, we do not lose the content that’s already on the gadget. Once an element has been successfuly loaded, it will be removed from the loadContainer element and placed in its correct location. Here’s what loadSmallChart() looks like:

    // Loads a "small" chart - places an IMG element in the content (in the thumbnail)

    // and sets its properties (URL etc)

    // It also sets up an event so that when the chart is done loading (onload), we will know and  be able to

    // adjust the content to the size of the chart.

    function loadSmallChart()

    {

        debugWrite("Loading small chart");

        var imageLoader = document.createElement("img");

        gebid("loadContainer").appendChild(imageLoader);

        imageLoader.style.width = "120px";

        imageLoader.onload = loadedElementComplete;

        imageLoader.targetParent = gebid("thumbnailContainerDiv");

        imageLoader.loadedIndex = displayThumbnail;

        imageLoader.src = config.get_resolvedThumbnailUrl();

    }

     

    The method creates an IMG tag and adds it to the loadContainer DIV. It then sets the parameters on the IMG and attaches a method to the onload event – this event will fire when the IMG tag is done loading the chart (at which stage we will go and update the gadget body to actually show the new chart). Note that we also that we add an expando attribute to the IMG tag we created which points to the element into which it will ultimately need to be added. This allows us to have a generic mechanism for moving the element later on. Here’s what loadedElementComplete looks like:

    // Used by the gadget as a callback to when a Range is done loading.

    function loadedElementComplete(element)

    {

        if (!element)

        {

            element = event.srcElement;

        }

     

        // Figure out where we need to place the DIV - this property was set up in the

        // loadRange() method to point to the target element.  

        var targetParent = element.targetParent;

       

        // Remove the "off screen" div from it's parent.

        element.parentElement.removeChild(element);

       

        // Now add it/replace the current existing child.

        if (targetParent.children.length == 1)

        {

            targetParent.replaceChild(element, targetParent.children[0]);

        }

        else

        {

            targetParent.appendChild(element);

        }

       

        // Specify that we did indeed load the range.

        loadedArray[element.loadedIndex] = true;

       

        // Make sure we know when we last updated.

        lastUpdate = new Date();

        setStatus();

       

        // Adjust the gadget to the content.

        adjustContent();

    }

     

     

    When loading a chart, the first part of the method will guarantee we point to the correct element that got loaded. Next, we figure out where the newly loaded element needs to go to (by accessing element.targetParent). We then remove the newly loaded element from its parent and add it to the actual location where it’s going to be viewed. Finally, we mark the element as loaded (loadedArray[element.loadedIndex] = true) and we call adjustContent to make sure the gadget is properly sized (note that in this call to adjustContent, the loaded state of the content will be set to true, and so the actual resizeing/adjusting part of the code will manifest.

    That’s it for now – tomorrow I will show how ranges are fetched and displayed (the mechanism is slightly more elaborate since we do not have an element we can use to load it (as we do with the IMG element for charts).

  • Coding the Excel Services Windows 7 Gadget – Part 1 - Settings

    In the next few days, my posts will revolve around the mechanisms that make the Excel Services gadget tick. I won’t talk a whole lot about Gadget development – that could easily take up 4 or 5 posts – I will give a very brief overview though on what a gadget is and how it works. You can read more about gadgets on msdn and on blogs around the tubes.

    Windows 7 (and vista) gadgets come in various flavors. By far the most popular are the ones based on HTML and Javascript because they are very easy to write and deploy. When writing a gadget in HTML, there are 3 basic parts that you need to code:

    1. What appears in the actual gadget: That’s the guy that’s usually visible on the desktop. It can have 2 “modes” – big/small, maximized/minimized or docked/undocked (in Vista you used to be able to dock it). The actual content is represented by an HTML that’s in the gadget package – the HTML gets loaded and from that point you can consider it the gadget “application”.

    2. The settings window: Gadgets have little wrench icons attached to them – clicking on that icon will bring up a form you can use to edit your settings. The infrastructure gives you the ability to save settings via special methods supplied off the Gadget object. The settings window is also essentially an HTML page that the Gadget HTML sets up as part of it’s initialization.

    3. The flyout window: Some gadgets have functionality where by you can click on some UI element and a “flyout” view will shoot out of the side of the gadget, allowing it to supply more information. The flyout appearance and disappearance is governed by the Gadget HTML (again, via special methods off the Gadget object available in the javascript OM).

    Each post will tackle one of these aspects of the gadget. I am not going to go too deeply into DHTML and the like (because DHTML and JS make me bleed from my eyes) – I will only go into the areas that I think are critical or especially interesting.

    This first post will talk about the settings window – this is what it looks like:

    image

    The settings page has two important parts to it. The first is the bit that knows how to take a workbook, extract the various items that are available in it and put them in the combo boxes for the user to select from. The second is actually writing the information to the gadget settings store so that it can be later extracted and used to display the appropriate information. By far the more interesting part here is the way we do the discovery. So lets delve into that. All the code for the Settings screen is encapsulated in the settings.js file in the gadget and the HTML that is displayed is the one contained in Settings.html (I am pretty imaginative with names!)

    When a user clicks on the “Refresh” button, the following JS code runs:

    // This call issues 4 async requests to the Excel Services REST API to discover what elements exist in the workbook.

    // An async call is made each to the Charts, Ranges, Tables and PivotTables.

    // when each call is done, it will call back into the onAtomRequestComplete function which will tally the results and take care

    // of updating the UI once done.

    // Each call takes the base URL (which is essentially http://[server]/_vti_bin/ExcelRest.aspx/[Path to workbook]) and appends the

    // appropriate URL for it.

    function refreshFromWorkbook()

    {

        feedsReturned = 0;

        var refreshReq = null;

        refreshHadErrror = false;

        refreshReq = new atomRequest(get_uiBaseDocument() + "/Model/Charts?___nocache___=" + new Date().getTime(), chartsIndex, onAtomRequestComplete);

        refreshReq.send();

        refreshReq = new atomRequest(get_uiBaseDocument() + "/Model/Ranges?___nocache___=" + new Date().getTime(), rangesIndex, onAtomRequestComplete);

        refreshReq.send();

        refreshReq = new atomRequest(get_uiBaseDocument() + "/Model/Tables?___nocache___=" + new Date().getTime(), tablesIndex, onAtomRequestComplete);

        refreshReq.send();

        refreshReq = new atomRequest(get_uiBaseDocument() + "/Model/PivotTables?___nocache___=" + new Date().getTime(), pivotTablesIndex, onAtomRequestComplete);

        refreshReq.send();

       

        // Update the UI to show that we are querying.

        setRefreshStatus("Connecting to server...");

        showRefreshIndicator(true);

        clearLists();

    }

     

     

    When the user asks to refresh from the workbook, we will issue four different discovery requests – one for each of the four resources we know we can represent on the gadget: Charts, ranges, tables and PivotTables. The gadget is using a class called “atomRequest” which is fairly straightforward wrapper around the XmlHttpRequest which is the class used to issue asynchronous HTTP requests via script. Because these are async requests, two things are apparent:

    1. They both need callbacks to be specified (so that we know when they are done running) and
    2. We can issue multiple requests at the same time, running them in parallel*.

    The get_uiBaseDocument() method call takes the name of the workbook specified in the text-box on the settings window and turns it into the REST URL base that we need. For each request we append the appropriate resource relative URL (/Modek/Charts for charts for example) and we also add another URL parameter called __nocache__ which is not used by REST, but will cause the system to not try and use cached versions of the returned data. Here’s the actual code behind get_uiBaseDocument() – as you can see it’s a fairly straightforward string manipulation method that sticks “_vti_bin/ExcelRest.aspx” in the middle of the URL. Could probably have been written more efficiently with some regex magic.

    // Takes a document URL (http://server/doclib/file.xlsx) and turns it into a REST URL

    // (http://server/_vti_bin/ExcelRest.aspx/doclib/file.xlsx)

    function baseRestFromDoc(doc)

    {

        if (doc.toLowerCase().indexOf("_vti_bin/ExcelRest.aspx") > -1)

        {

            return doc;

        }

     

        var newUri = new String();

        var startIndex;

        if (doc.substr(0, 7).toLowerCase() == "http://")

        {

            newUri = doc.substr(0, 7).toLowerCase();

            startIndex = 7;

        }

        else if (doc.substr(0, 8).toLowerCase() == "https://")

        {

            newUri = doc.substr(0, 8).toLowerCase();

            startIndex = 8;

        }

        else

        {

            return "";

        }

     

        doc = doc.substr(startIndex);

     

        var server = doc.substr(0, doc.indexOf("/"));

     

        newUri += server;

     

        newUri += "/_vti_bin/ExcelRest.aspx";

     

        newUri += doc.substr(server.length);

        return newUri;

    }

    All four async requests have the same callback. That callback will track which one return and once all are done, will continue to the next step:

    // This is the callback that gets invoked when each of the ATOM request is done.

    // The atomRequest is the instance of the JS object we use to make calls - it is defined in utils.cs/

    // This method keeps tally on how many calls came back. Once all 4 come back, it updates the UI appropriatley.

    function onAtomRequestComplete(atomRequest)

    {

        if (atomRequest.hasError || refreshHadErrror)

        {

            // If we errored out, mark the global as such.

            refreshHadErrror = true;

        }

        else

        {

            // Otherwise retain the result - we will use it once all 4 requests are done.

            refreshResult[atomRequest.userState] = atomRequest;

        }

     

        // Tally how many came back.

        feedsReturned++;

     

        // Once all feeds came back, update the UI.

        if (feedsReturned == feedCount)

        {

            showRefreshIndicator(false);

            if (refreshHadErrror)

            {

                // We had an error - set the status line to say that.

                setRefreshStatus("There was an error trying to fetch charts.", "red");

            }

            else

            {

                // Need to fill the selection.

                fillSelect(refreshResult[chartsIndex], config.typeChart, "Charts");

                fillSelect(refreshResult[rangesIndex], config.typeRange, "Ranges");

                fillSelect(refreshResult[tablesIndex], config.typeTable, "Tables");

                fillSelect(refreshResult[pivotTablesIndex], config.typePivotTable, "PivotTables");

                setRefreshStatus("You can now select elements from the workbook.");

            }

        }

    }

    For each of the async calls that comes back, the callback checks on the error status and stores the result in a global array (refreshResult). It also counts how many requests have come back already. When the number that comes back is the number we issued, the code will take the entire result set and update the UI with it.

    The fillSelect() method simply takes the ATOM feed that is returned, iterates over the elements that are there and adds them to the combo-boxes under the appropriate header so that you get the following result in the combo-box:

    image

    And that’s about it for the REST interaction in here. Once the user hits “OK”, the gadget uses a class called config (defined in config.js) to store the data into the settings repository of the gadget. The Gadget is also listening to the OK button and will update once it’s pressed (showing the data the user requested).

    As you can see, the code needed for doing discovery is relatively simple. The missing part is the atomRequest class which in itself is fairly simple and the code that adds elements into the combo boxes which is not Excel Services specific.

    Next up, updating the gadget to show the actual data/charts!

    * Well, semi-parallel – the gadget infrastructure may or may not limit how many opened connections we can have concurrently.

  • How to seamlessly embed data from Excel Services spreadsheets into Word

    One of the fun things about working on the programmability aspects of Excel Services is realizing just how many interesting scenarios exist that you never thought of originally. I got an IM from Dany Hoter asking for help on an issue he ran into with one of his customers. I have been working with Dany for about 10 years now and he always has interesting questions and ideas. This time around, the problem revolved around a complicated word document that contained links to Excel data – the word document would update and grab data from the workbook via automation (OLE embedding essentially). For a number of reasons, Dany wanted to remove the dependency on the Excel client and was wondering how to go about it. Eran from the Excel Services team suggested looking into fields and that’s how Dany landed in my IM window.

    This is where the fun part begins. In Word, one can insert fields into the document that have certain functionality. The most used ones are probably the document Index, page numbers and Date/Time. One of those fields is an IncludeText field which can take text from various sources and put it inside the field. To add such a field, you use the “Quick Parts” drop-down in the “Insert” ribbon tab:

    image

    Once selected, you are presented with a slew of options – the one you want is the “IncludeText” field:

    image

    When the IncludeText field is selected, the user is presented with two more panes – the file name or URL from which to take the data (that’s in the middle) and the options of the field (that’s on the right).

    In the URL field, we supply an Excel REST URL that points to our desired cell – this should be a REST URL that’s presented in the Atom format – that way we can traverse the data and get back what we want. In my post about getting range data from Excel Services, there’s an example for such a link. Here’s what the link looks like for our case here:

    http://tsdlt2/_vti_bin/ExcelRest.aspx/docs/Bugs.xlsx/Model/Ranges('PersonalBugCount')?$format=atom

    As you can see, we are going to a spreadsheet called “Bugs.xlsx” and asking for the range called “PersonalBugCount”. The parameter $format is set to atom to instruct Excel Services to return an XML representing the data. The result (removed some elements for brevity) looks like this:

    <?xml version="1.0" encoding="utf-8"?>

    <entry xmlns:x="http://schemas.microsoft.com/office/2008/07/excelservices/rest" xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservice" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">

      <title type="text">AverageBugCount</title>

      <!-- Removed stuff -->

      <content type="application/xml">

        <x:range name="AverageBugCount">

          <x:row>

            <x:c>

              <x:v>1.3</x:v>

              <x:fv>1.3</x:fv>

            </x:c>

          </x:row>

        </x:range>

      </content>

    </entry>

    We now need to set two more fields in the dialog – the namespace mappings and the XPath expression – luckily, for the case where you want a single value, both of these are the same and require no modification.

    Namespace Mapping is used in conjunction with the XPath expression – the XPath expression needs to know what aliases to use for what namespaces. In this case, we have two namespaces we need to use:

    http://www.w3.org/2005/Atom – this is the namespace for the ATOM scheme.

    http://schemas.microsoft.com/office/2008/07/excelservices/rest – this is the namespace for the Excel REST data scheme.

    Both of these will need to have aliases associated with them (the aliases are not really that important). The setting that should be entered in the “Namespace mapping” field is then:

    This goes in the “Namespace Mappings” field:

    xmlns:a="http://www.w3.org/2005/Atom" xmlns:x="http://schemas.microsoft.com/office/2008/07/excelservices/rest"

    The XPath needs to point to the “fv” element that’s in the XML and so it should be set to:

    This goes in the “XPath Expression” field:

    /a:entry/a:content/x:range/x:row/x:c/x:fv

    And that’s pretty much it! Once you hit OK, the dialog will close and you should see the value from the workbook. Here’s text from a word document with fields toggled to see the codes:

    image

    And here’s the text as it appears when the fields are shown as text:

    image

    The highlighted areas are the values that came from Excel Services. The cool part is that when the workbook updates and the word document is updated, this data will update as well, showing the new values!

    Now, another nifty aspect of this is that the same mechanism can be used to embed richer data – like HTML. If you will recall, at the bottom of this post I showed how one can access Ranges from Excel and get back HTML fragments. Guess what – Word knows how to embed those as well. Using the same mechanism, we can enter a field and point it to a REST URL returning an HTML fragment. In this case, we do not need the namespace mappings or the XPath expression – just the URL (in this example a URL to a range that looks like a score-card):

    http://tsdlt2/_vti_bin/ExcelRest.aspx/Docs/Bugs.xlsx/Model/Ranges('PersonalScoreCard')

    The URL resolves to the following HTML:

     
     
     
     
    Shaharp:
    10
     
    Average:
    1.3

    When embedded in the workbook by using the IncludeText field, this is what it looks like:

    image 

    The same thing applies as before – if the data of the workbook changes and the scorecard data and formatting changes, the next time the Word document will be updated, this text and formatting will also be updated.

    Finally, if you look just above the IncludeText field, you will see an IncludePicture field – using that allows you to embed live images from Excel Services within the workbook – in this example, the URL we will embed is the chart that shows the progress made on the bugs and the goals:

    image

    To summarize – as you can see, you can create richer and self-updating documents in word by using fields that connect to Excel data and bring back plain text (the first example with the Atom) or formatted text (the second example with the HTML fragment) or charts (last example). These can be updated at will just like any other field in Excel and guarantee that your document will contain data that is up to date and is synchronized with the source – no more checking to see if the data was copied properly etc.

  • Bringing it all back home – using advanced REST functionality with the Excel Services Gadget

    In the past two weeks I detailed both the new REST functionality in Excel Services as well as the Excel Services Gadget (which will hopefully be available for download soon). In the previous post I showed how you can use URL parameters to REST URLs to modify the result you get back. In this post, I will show how you can create an incredibly compelling story for users by using both that and the gadget that I introduced earlier this week.

    To start, I have a workbook that contains information about my team bug counts. On top of everything else inside that workbook, it also contains a cell where, when an alias (email) is entered, pars of the workbook recalculate to show the bug trend/counts for that specific person:

    image

    If you click on the image, you will see the cell called “Alias” (the one that currently has “shaharp” inside it). the green table you see contains information specific to the Alias cell (so, if you change the cell to another value, the data will update accordingly). On top of that we also have a table of links in the workbook:

    image

    For each person there are two links – one under the alias of the person and the other called “Home Site”. Here are the formulas for both:

    Under the alias, the formula is: =HYPERLINK("http://tsdlt2/_vti_bin/ExcelRest.aspx/Docs/TeamBugs.xlsx/Model/Charts('PersonalBugs')?Ranges('Alias')=" & A1, A1)

    Under the Home Site the formula is:=HYPERLINK("http://tsdlt2/PersonalPages/" & A1 & ".aspx", "Home Site")

    As you can see, the first formula resolves to a hyperlink to a REST URL where the object shown is the same “PersonalBugs” chart but where for each person, a value is set into the “Alias” cell. In this way, a single workbook/chart can serve multiple purposes. Here, we pass in the alias to a cell and then get the chart that corresponds to that person.

    Here’s how this works. First, you can see that the link show up in the gadget itself:

    image

    Now, remember, there are two links here. The Home Site one is just a run of the mill link and when clicked, will open up in your default browser:

    image

    As you can see, the link corresponds to an alias which then opens up in the browser according to the formula.

    The interesting part is what we can do with the other link though. The gadget recognizes that the link is actually a REST link – and so it decides that it can actually display it as part of the gadget in a flyout:

    image

    Once the gadget loses it’s focus, or you click inside it, the flyout will collapse back into the gadget.

    And that’s it for this week. This week I tried to show you some interesting ways of using the new Excel Services REST APIs and some of the new and exciting opportunities it gives developers and users. There was a post about the “essentials” of REST in Excel Services – a kind of “cheat sheet” for all the things REST can do. There were two posts about the Excel Services Gadget which showed the concept and then a post that explained the more advanced stuff REST can do by passing parameters into the workbook via the URL.

    Next week will show the actual code running behind the gadget. Have a good weekend!

More Posts Next page »

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker