Register | Login

Stacking Code

public interface IBlog { string Dump(Stream consciousness); }

got pivot?

Wednesday, 29 December, 2010 @ 10:17 PM < Adam Boddington
Tags: Building Neno, Pivot

This is post #30 in the Building Neno series. Please click here for a description of the Building Neno project and instructions on how to access the source code for this post.

For me the word "pivot" doesn't conjure up images of pivot tables, fulcrums, or anything you would normally expect. Up until recently, if you had said "pivot", I would've thought of this bit of classic television.

But no longer. If you've not heard of Pivot, it's a nifty bit of software by the (now defunct) Microsoft Live Labs team. Initially there was a Silverlight control (PivotViewer), a Windows 7 application, and an Excel plugin. Of the three, it seems only PivotViewer has survived the Live Labs breakup with a transition to the Silverlight team.

So what is Pivot? Pivot is a unique way to browse and search large amounts of data, data with a visual component. By throwing all the information on the screen using Deep Zoom technology, the user can flick through the data and zoom in and out using the mouse. They can also filter, sort and group by any available facet of the information. I recommend checking out the original Live Labs videos for a jaw dropping demo.

So you may have guessed, I want to add Pivot to my blog. I have 29 posts so far (30 with this one), which should be a decent enough dataset to make Pivot worthwhile. As the post count grows, Pivot will be become indispensable for browsing and searching the content quickly and easily. When I want to lookup that thing I did, with that other thing, two years ago, Pivot will take me straight there. Google probably will too, but Pivot will do it in style!

Screenshots

The first thing I need to do is get a visual component for my Pivot collection. A plain screenshot of each blog post will do for now, at a resolution that will make it possible to read what is captured. I'll go for 1024x1024 pixel images, saved as PNGs.

It would be neat to make this image creation process automatic, but I don't want to get into that just yet. Manual screenshots from Firefox will do for now. I'll use the Web Developer plugin to set the window to 1024 pixels wide, FireShot to take the screenshots, and Paint.NET to crop the height and save the PNGs. I'll upload each screenshot as an attachment to each post with a slug of "pivot-screenshot".

Pivot Collection Generation

The next step is to create the Pivot collection from code. There are some tools to help with that, notably Pivot Collection Tool (a.k.a. Pauthor) and Pivot Collection Tools. In the end, I liked the look of the basic collection in the Pivot Collection Tools, but I felt it could be much simpler. So I rolled my own, based on their ideas.

The Pivot collection generation library is a standalone assembly in Moja, StackingCode.Moja.Pivot. The idea is to take a typed collection of any POCO and transform it into a Pivot collection by annotating the POCO's properties with custom attributes from the StackingCode.Moja.Pivot assembly. The attributes define which properties on the POCO map to which elements in the Pivot collection. Right now there are attributes defined for the following Pivot collection elements:

  • ImagePath
  • Name
  • Href
  • Description
  • Facet
  • Tags

The PivotItemFacetAttribute is the only attribute that can be placed on more than one property. Facets are the additional properties of objects to be displayed in the collection, which can also be used to sort, filter and group the collection. The PivotItemFacetAttribute takes a name (defaults to the name of the property if one isn't provided), type, and whether the property is IEnumerable. The following facet types are supported by Pivot collections:

  • DateTime (XSD DateTime)
  • Link
  • LongString (text longer than a short sentence, used in the info panel only)
  • Number
  • String

The PivotItemTagsAttribute can only be placed on an IEnumerable property, which can be empty. I'm not sure why Pivot has tags when they could just be an IEnumerable facet, but it does. I found out the hard way that having a facet called Tags will cause an error -- so I made the PivotItemTagsAttribute to handle that special case instead.

In this assembly, the PivotCollection class does all the work. Once given a collection of annotated objects, it uses Deep Zoom Tools to create a series of images and the related image collection. The PivotCollection class uses an XmlWriter to create the Pivot collection itself.

Check out the Pivot collection schema if you want to see what it's trying to create.

Unfortunately the Deep Zoom Tools expect everything to be a local file. I want my images to be downloaded from a URL. So I made another Pivot collection class, Web.PivotCollection, to handle that special scenario. The Web.PivotCollection class downloads each image to a local file, then continues on as normal.

Check out the source if you're interested in how everything works. You can even use the assembly to create your own Pivot collections.

Annotating Post

I expect attributes in the Pivot collection items to tell me what goes where. But I don't want to put those attributes in my post domain class. I'll use an adapter instead.

public class PivotPost
{
    public PivotPost(Post post)
    {
        Post = post;
    }

    [PivotItemFacet(FacetType.String)]
    public string Author
    {
        get { return Post.Author.DisplayName; }
    }

    [PivotItemFacet(FacetType.DateTime, Name = "Publish Date and Time")]
    public string PublishDateTimeOffset
    {
        get { return Post.PublishDateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ss"); }
    }

    [PivotItemName]
    public string Title
    {
        get { return Post.Title; }
    }

    [PivotItemTags]
    public IEnumerable<string> Tags
    {
        get { return Post.Tags.Select(tag => tag.Name); }
    }

    [PivotItemHref]
    public string Url
    {
        get
        {
            Uri url = HttpContext.Current.Request.Url;
            string baseUrl = url.AbsoluteUri.Substring(0, url.AbsoluteUri.Length - url.AbsolutePath.Length);
            var publishDateAndSlug = new PublishDateAndSlug(Post);

            return baseUrl + string.Format("/blog/{0}/{1}/{2}/{3}", publishDateAndSlug.Year, publishDateAndSlug.Month, publishDateAndSlug.Day, publishDateAndSlug.Slug);
        }
    }

    [PivotItemImagePath]
    public string PivotScreenshotUrl
    {
        get { return Url + "/pivot-screenshot"; }
    }

    private Post Post { get; set; }
}

Fire and Forget

Now I can create an action to kick everything off. I'll put this in a new PivotController class and make sure only administrators can access it.

[UserIsAnAdministrator]
public ActionResult GenerateCollection()
{
    try
    {
        IEnumerable<PivotPost> items = Container.Get<IPostService>().GetPosts()
            .Where(post => post.IsPublished)
            .Where(post => post.Attachments.Any(attachment => attachment.Slug == "pivot-screenshot"))
            .ToArray()
            .Select(post => new PivotPost(post));

        var pivotCollection = new PivotCollection<PivotPost>("Stacking Code", items, 1024);
        string collectionPath = Path.Combine(Server.MapPath("~/content/pivot"), DateTime.Now.ToString("yyyyMMddHHmmss"), "Collection.cxml");
        pivotCollection.WriteToFile(collectionPath);
        Messages.Add("Pivot collection successfully generated.");
    }
    catch (Exception exception)
    {
        Messages.Add(exception);
    }

    return View();
}

I'm only using published posts that have a pivot screenshot when I should probably be using all published posts and just show a "no screenshot" image for those missing one.

The collection is getting written out to /content/pivot in a subdirectory based on the current date and time (to the second). My Pivot collection is static -- I'll need to regenerate it whenever a new post is added -- but I don't want to mess with a collection someone might be using at the time. The solution is to shift the new version into its own folder and have the PivotViewer control look for the latest version it can find upon loading. This means I have to manually clean up old versions occasionally, but perhaps I can make that automatic later on.

Pivot Generate Collection

That's Quite a View

The final step is to place the Silverlight PivotViewer control into a Silverlight application, then host that application in a page in my web application. There are plenty of examples of how to do just that out there on the web, so I won't bore you with the nitty details. You can check out the source for Neno if you need a reference.

If you're doing something similar, however, there are a couple of gotchas to look out for. Make sure all the Pivot assemblies are available to your Silverlight application, not just System.Windows.Pivot. You can just reference System.Windows.Pivot and the rest will be pulled in when you compile.

  • System.Windows.Pivot
  • System.Windows.Pivot.Model
  • System.Windows.Pivot.SharedUI
  • System.Windows.Pivot.StringResources
  • System.Windows.Pivot.Utilities

Also, make sure the Silverlight 4 Toolkit is installed. I only had the Silverlight 4 SDK and was getting really weird errors when running the application. Installing the Silverlight 4 Toolkit (April 2010) made them go away.

The only code that I put in the Silverlight application was to load the collection and handle link clicks.

public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();

        Loaded += MainPage_Loaded;
        PivotViewer.LinkClicked += PivotViewer_LinkClicked;
    }

    private void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        string collectionPath = Application.Current.Host.InitParams["CollectionPath"];
        PivotViewer.LoadCollection(collectionPath, string.Empty);
    }

    private void PivotViewer_LinkClicked(object sender, LinkEventArgs e)
    {
        HtmlPage.Window.Navigate(e.Link, "_blank");
    }
}

The collection path is set by the action before the view is rendered. (This is where it finds the latest version of the collection.)

public ActionResult Index()
{
    try
    {
        string pivotPath = Server.MapPath("~/content/pivot");
        DirectoryInfo collectionPath = new DirectoryInfo(pivotPath).GetDirectories().OrderBy(cp => cp.Name).LastOrDefault();

        if (collectionPath == null)
            throw new Exception("No pivot collection.");

        Uri url = Request.Url;
        string baseUrl = url.AbsoluteUri.Substring(0, url.AbsoluteUri.Length - url.AbsolutePath.Length);

        ViewData["CollectionPath"] = baseUrl + "/content/pivot/" + collectionPath.Name + "/Collection.cxml";

        return View();
    }
    catch (Exception exception)
    {
        Messages.Add(exception);

        return RedirectToAction("index", "home");
    }
}

The view passes it on to the Silverlight application like this...

<param name="initParams" value="CollectionPath=<%:ViewData["CollectionPath"]%>" />

And the end result can be found here. Click on the link and have fun.

There are 0 comments.


Comments

Leave a Comment

Please register or login to leave a comment.


Older
Prettify and Internet Explorer

Newer
Screenshot Generator

Older
Prettify and Internet Explorer

Newer
Screenshot Generator

browse with Pivot


About


Projects

Building Neno


RSS
Recent Posts

Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue


Tags

Architecture (13)
ASP.NET (2)
ASP.NET MVC (13)
Brisbane Flood (1)
Building Neno (38)
C# (4)
Challenges (3)
Collections (1)
Communicator (1)
Concurrency Control (2)
Configuration (1)
CSS (5)
DataAnnotations (2)
Database (1)
DotNetOpenAuth (2)
Entity Framework (1)
FluentNHibernate (2)
Inversion of Control (5)
JavaScript (1)
jQuery (4)
Kata (2)
Linq (7)
Markdown (4)
Mercurial (5)
NHibernate (20)
Ninject (2)
OpenID (3)
OS X (1)
Pivot (6)
PowerShell (8)
Prettify (2)
RSS (1)
Spring (3)
SQL Server (5)
T-SQL (2)
Validation (2)
Vim (1)
Visual Studio (2)
Windows Forms (3)
Windows Service (1)


Archives


Powered by Neno, ASP.NET MVC, NHibernate, and small furry mammals. Copyright 2010 - 2011 Adam Boddington.
Version 1.0 Alpha (d9e7e4b68c07), Build Date Sunday, 30 January, 2011 @ 11:37 AM