Creating the MSDN Magazine Pivot Client

When non-developers approach Pivot, they tend to be dismayed by 2 primary things:

  • it’s not something that you can just plug into your data source and have it work
  • it’s not some discrete thing that you can just slap onto a Web page and have it work

We’ve talked about the data aspects of building the MSDN Magazine collection in previous posts.  What I want to review in this post is the actual client work required to make the show the collection.  There are actually 2 parts to the client work: the Silverlight project which extends the Silverlight Pivot viewer control, and the JavaScript which creates the Silverlight object and listens to events on it.

Creating the Silverlight XAP

When you download the Pivot SDK, you’ll get the Pivot viewer control – just a standard Silverlight control.  Most of the work, then is just packaging that control into a xap file by wrapping it into a Silverlight project and putting it on the mainpage.xaml file (or whatever you’ve declared as your applications RootVisual object).  From there, you basically just write code to control the behavior of the control (e.g. what cxml to load, how to handle errors, catching events).  As you can see, there’s really not all that much to it:

pivot-viewer-sl-solution

That said, there are a couple interesting extensions that I did to enable some capabilities on the control.  Specifically, I derived my own control from PivotViewer so that I could hook a protected method and execute conditional logic for the action buttons on a trade card.  I also made the Silverlight object scriptable and added a bunch of events so that could better instrument the Pivot experience with our BI system.

Inheriting from PivotViewer

As you may have noticed in the MSDN Magazine collection, a trade card can have up to 2 possible action buttons: one button to go read the article and another one to go and get associated sample code. 

action-buttons

However, not every article has sample code, and as such, I need to be able to execute some conditional logic to determine whether or not to display the action button.  You would hope/expect that the PivotViewer control would expose a method or event which you could hook per collection item that would enable you to specify the custom actions for that item – and you would be right – kind of.  The PivotViewer does expose this in the form of a method override.  The problem is that it’s a protected method.  As a result, I created a custom class deriving from PivotViewer so that I could override the appropriate method and basically expose that method’s functionality to consumers of my custom PivotViewer.

public class CustomPivotViewer : System.Windows.Pivot.PivotViewer
{
    public Func<string, List<CustomAction>> GetCustomActionsForItemCallback { get; set; }

    protected override List<CustomAction> GetCustomActionsForItem(string itemId) {
        List<CustomAction> customActions = null;

        if (GetCustomActionsForItemCallback != null)
            customActions = GetCustomActionsForItemCallback(itemId);

        return customActions;
    }
}

Back in my MainPage.xaml.cs code behind file, then (where I manipulate the instance of my custom PivotViewer object), I then have the following:

private List<CustomAction> GetCustomActionsForItem(string itemId) {
    var customActions = new List<CustomAction>();

    var item = MagazinePivotViewer.GetItem(itemId);
    if (item.Facets[FACET_KEY_HAS_CODE].First() == FACET_VALUE_HAS_CODE)
        customActions.Add(_getCodeAction);

    if (!string.IsNullOrEmpty(item.Href))
        customActions.Add(_readArticleAction);

    return customActions;
}

As the PivotViewer control brings each item’s trade card into focus, it calls the GetCustomActionsForItem method, which my custom class overrides and then delegates to my code behind class using a callback.  In my code behind class, that callback function runs the necessary conditional logic and returns a list containing the appropriate actions.  The action instances themselves are  created during xap initialization and are held in static fields for improving performance.

Making it Scriptable

The other major way that I’ve extended the default Pivot viewer functionality is to make my xap scriptable to my HTML page, and forward events coming from the viewer control out to the page.  I did this for the MSDN Magazine collection specifically so that I could wire the control up to our business intelligence service (we use Omniture).  In case you’ve never had to deal with one of these systems (Omniture, WebTrends, etc.), they don’t just sit on the back end and parse the Web server’s log files.  Rather, they rely on an additional HTTP call from the client to the service’s servers.  This level of indirection enables the site owners to provide additional detail which can then be used to slice and dice blunt stats such as page views when performing analysis.  For the Pivot viewer, I wanted to feed into the analytics system was not just how many people were using the Pivot experience, but *how* they were using it (e.g. are there specific facet categories that are used more than others, do people drill-through or re-pivot using the info pane, etc.).  This insight can then be used to drive things like the order in which I show facet categories (or whether I can get rid of categories) and what features I add going forward.

Making the Silverlight object scriptable was pretty straightforward.  The first thing to do is register my xap’s mainpage object as a scriptable object using a static method on the HtmlPage class:

HtmlPage.RegisterScriptableObject("PivotViewer", this);

Next, it was just a matter of creating all of the events that I wanted to expose as scriptable.  The only requirement that goes beyond the normal pattern for exposing events in .NET is to ensure that the event declaration is decorated with the ScriptableMemberAttribute:

[ScriptableMember]
public event EventHandler<PivotViewerStateEventArgs> ArticleButtonClicked;

[ScriptableMember]
public event EventHandler<PivotViewerStateEventArgs> ArticleLinkClicked;

[ScriptableMember]
public event EventHandler<PivotViewerStateEventArgs> CodeButtonClicked;

[ScriptableMember]
public event EventHandler<PivotViewerStateEventArgs> FiltersChanged;

[ScriptableMember]
public event EventHandler<PivotViewerStateEventArgs> SortChanged;

[ScriptableMember]
public event EventHandler<PivotViewerStateEventArgs> ViewChanged;

protected virtual void OnArticleButtonClicked(string articleUrl) {
    if (ArticleButtonClicked != null)
        ArticleButtonClicked(this, new PivotViewerStateEventArgs { ArticleHref = articleUrl });
}

protected virtual void OnArticleLinkClicked(string articleUrl) {
    if (ArticleLinkClicked != null)
        ArticleLinkClicked(this, new PivotViewerStateEventArgs { ArticleHref = articleUrl });
}

protected virtual void OnCodeButtonClicked(string articleUrl, string codeUrl) {
    if (CodeButtonClicked != null)
        CodeButtonClicked(this, new PivotViewerStateEventArgs { ArticleHref = articleUrl, CodeHref = codeUrl });
}

protected virtual void OnFiltersChanged(string filterString) {
    if (FiltersChanged != null)
        FiltersChanged(this, new PivotViewerStateEventArgs { FilterCategories = filterString });
}

protected virtual void OnSortChanged(string sortFacet) {
    if (SortChanged != null)
        SortChanged(this, new PivotViewerStateEventArgs { SortFacet = sortFacet });
}

protected virtual void OnViewChanged(string view) {
    if (ViewChanged != null)
        ViewChanged(this, new PivotViewerStateEventArgs { View = view });
}

Because the events pass a custom EventArguments type, we need to also ensure that this custom type is decorated with ScriptableTypeAttribute and its members are decorated with ScriptableMemberAttribute:

[ScriptableType]
public class PivotViewerStateEventArgs : EventArgs
{
    [ScriptableMember]
    public string ArticleHref { get; set; }

    [ScriptableMember]
    public string CodeHref { get; set; }

    [ScriptableMember]
    public string FilterCategories { get; set; }

    [ScriptableMember]
    public string SortFacet { get; set; }

    [ScriptableMember]
    public string State { get; set; }

    [ScriptableMember]
    public string View { get; set; }
}

Firing the event is consistent with everything you already know from .NET programming – nothing more to see here.

Consuming the Silverlight

Because the xap completely encapsulates the Pivot viewer control, the configuration of all inputs is performed by default in the code behind the mainpage.xaml file.  In my case, I wanted to have some of these configuration options available through the creation script or markup.  To enable this, I enabled several different inputs (including the location of the cxml and the default view state) to be passed in through the Silverlight object’s InitParams collection.  This is a comma separated list that the application host automatically parses and makes available as a dictionary – through the Application.Current.Host.InitParams object.  If you’re creating the Silverlight object using the object tag, you can specify all the configuration options using a param element.  In my case, I wrapped the client functionality in a Javascript object, and am therefore creating it using the following:

PivotViewer.prototype.renderIn = function (container) {
    var self = this;
    Silverlight.createObject(
                this.xapPath,
                container[0],
                "magazinePivotViewer_SL",
                {
                    type: "application/x-silverlight-2",
                    width: "100%",
                    height: "100%",
                    background: "white",
                    version: "4.0.50401.0",
                    enableHtmlAccess: 'true',
                    autoUpgrade: 'true'
                },
                {
                    onError: this.onSilverlightError,
                    onLoad: function (sender, e) {
                        self.onLoad(sender, e);
                    }
                },
                "collection=" + this.collectionPath + ",defaultViewState=" + this.defaultViewState,
                this);
}

If you take a look at the third line from the bottom, you can see where the init parameters is built up from some fields (which were set during the object initializer). Another function of the initializer is that it allows you to pass in JavaScript functions to handle the various state change events that I previously exposed in my mainpage.xaml code behind.

var PivotViewer = function (xapPath,
        collectionPath,
        defaultViewState,
        onArticleLinkClicked,
        onArticleButtonClicked,
        onCodeButtonClicked,
        onSortChanged,
        onViewChanged,
        onFiltersChanged) {
    this.xapPath = xapPath;
    this.collectionPath = collectionPath;
    this.defaultViewState = defaultViewState;

    this.onArticleLinkClicked = onArticleLinkClicked;
    this.onArticleButtonClicked = onArticleButtonClicked;
    this.onCodeButtonClicked = onCodeButtonClicked;
    this.onSortChanged = onSortChanged;
    this.onViewChanged = onViewChanged;
    this.onFiltersChanged = onFiltersChanged;

};

In order to wire these client side functions up to the scriptable events in my Silverlight code, I’m hooking the load event in the Silverlight initialization code, and then setting the input JavaScript functions to the Silverlight event members.

PivotViewer.prototype.onLoad = function (sender, e) {
  sender.Content.PivotViewer.ArticleLinkClicked = this.onArticleLinkClicked;
  sender.Content.PivotViewer.ArticleButtonClicked = this.onArticleButtonClicked;
  sender.Content.PivotViewer.CodeButtonClicked = this.onCodeButtonClicked;
  sender.Content.PivotViewer.FiltersChanged = this.onFiltersChanged;
  sender.Content.PivotViewer.ViewChanged = this.onViewChanged;
  sender.Content.PivotViewer.SortChanged = this.onSortChanged;
}

This then gives me the ability to react in my HTML page to the state change events that are happening in the Pivot viewer Silverlight control. For example, in order to create a state change “console”, I would initialize my Pivot collection using the following JQuery:

$(document).ready(function () {
    var sl = new PivotViewer("ClientBin/Magazines.PivotViewer.xap",
        "http://az7446.vo.msecnd.net/msdn-magazine/msdnmagazine.cxml", 
        "%24facet0%24=Issue%20Date&%24view%24=2",
        function (sender, e) { $("div#console").append("articleLinkClicked: " + e.ArticleHref + "<br/>"); },
        function (sender, e) { $("div#console").append("articleButtonClicked: " + e.ArticleHref + "<br/>"); },
        function (sender, e) { $("div#console").append("codeButtonClicked: [article - " + e.ArticleHref + "] 1" + "<br/>"); },
        function (sender, e) { $("div#console").append("sortChanged: " + e.SortFacet + "<br/>"); },
        function (sender, e) { $("div#console").append("onViewChanged: " + e.View + "<br/>"); },
        function (sender, e) { $("div#console").append("OnFiltersChanged: " + e.FilterCategories + "<br/>"); })
            .renderIn($('div#silverlightControlHost'));
});

As I've mentioned before, my plan is to make all of the source code available to you through bitbucket (I need to do a little refactoring to get rid of some hard coded authentication strings and then I'll post the location). However, I hope this gives you a good idea of what was involved in creating the Pivot UX parts of the MSDN Magazine pivot collection.

About Howard Dierking

I like technology...a lot...
This entry was posted in MSDN Magazine, Pivot. Bookmark the permalink. Follow any comments here with the RSS feed for this post.