Pages

Monday 12 July 2010

Faking PivotViewer in Blend 4

If you've been using the new PivotViewer Silverlight control then you've probably come across the Blend problem. Basically, it doesn't work and you get the following error...

"Error HRESULT E_FAIL has been returned from a call to a COM component."

This problem does not occur in Visual Studio 2010 (VS2010) - although the page view does look a little suspect. Some people have suggested commenting out the PivotViewer element when opening the page in Blend, but there's a much better approach - fakes.

First, create a new "Silverlight Class Library" project in VS2010 called FakePivot and, once loaded, delete the Class1.cs file. Now add a new Code File called PivotViewer.cs with the following starting code:

using System;

namespace FakePivot
{
    public class PivotViewer
    {

    }
}

Next add a reference to the PivotViewer library (System.Windows.Pivot.dl) and the SharedUI library (System.Windows.Pivot.SharedUI.dll). If you don't find them under the .Net tab you can browse for them at <Program Files>\Microsoft SDKs\Silverlight\v4.0\PivotViewer\Jun10\Bin\. Now update your PivotViewer class to look like this...

using System;

namespace FakePivot
{
    public class PivotViewer : System.Windows.Pivot.PivotViewer
    {

    }
}

Right click on Microsoft's PivotViewer and choose "Go To Definition". You should now see the following metadata file:

#region Assembly System.Windows.Pivot.dll, v2.0.50727
// C:\Program Files\Microsoft SDKs\Silverlight\v4.0\PivotViewer\Jun10\Bin\System.Windows.Pivot.dll
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Resources;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;

namespace System.Windows.Pivot
{
    [ScriptableType]
    [TemplatePart(Name = "PART_Container", Type = typeof(Grid))]
    public class PivotViewer : Control, INotifyPropertyChanged
    {
        public PivotViewer();
        public PivotViewer(ResourceDictionary colorScheme);

        public IDictionary<string, IList<string>> AppliedFilters { get; }
        public int CollectionItemCount { get; }
        public string CollectionName { get; }
        public Uri CollectionUri { get; }
        public string CurrentItemId { get; set; }
        public ICollection<string> InScopeItemIds { get; }
        public string SortFacetCategory { get; }
        public string ViewerState { get; }

        public event EventHandler CollectionLoadingCompleted;
        public event EventHandler<CollectionErrorEventArgs> CollectionLoadingFailed;
        public event EventHandler<ItemActionEventArgs> ItemActionExecuted;
        public event EventHandler<ItemEventArgs> ItemDoubleClicked;
        public event EventHandler<LinkEventArgs> LinkClicked;
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual List<CustomAction> GetCustomActionsForItem(string itemId);
        public PivotItem GetItem(string id);
        public void LoadCollection(string collectionUri, string viewerState);
        public override void OnApplyTemplate();
        public static void SetResourceManager(ResourceManager resourceManager);
    }
}


What we want to do next is extract the interface for the standard PivotViewer class. If you have a refactoring tool I'd use it, otherwise you just have to do it manually. Either way, we should now have an interface in our project called IPivotViewer.cs ...

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Pivot;

namespace FakePivot
{
    public interface IPivotViewer : INotifyPropertyChanged
    {
        IDictionary<string, IList<string>> AppliedFilters { get; }
        int CollectionItemCount { get; }
        string CollectionName { get; }
        Uri CollectionUri { get; }
        string CurrentItemId { get; set; }
        ICollection<string> InScopeItemIds { get; }
        string SortFacetCategory { get; }
        string ViewerState { get; }

        event EventHandler CollectionLoadingCompleted;
        event EventHandler<CollectionErrorEventArgs> CollectionLoadingFailed;
        event EventHandler<ItemActionEventArgs> ItemActionExecuted;
        event EventHandler<ItemEventArgs> ItemDoubleClicked;
        event EventHandler<LinkEventArgs> LinkClicked;
        
        PivotItem GetItem(string id);
        void LoadCollection(string collectionUri, string viewerState);
    }
}

Next, we want to go back to our PivotViewer class and make it extend Control and implement our new IPivotViewer interface like this...

using System;
using System.Collections.Generic;
using System.Windows.Pivot;
using System.ComponentModel;
using System.Windows.Controls;

namespace FakePivot
{
    public class PivotViewer : Control, IPivotViewer
    {
        public IDictionary<string, IList<string>> AppliedFilters { get { throw new NotImplementedException(); } }

        public int CollectionItemCount { get { throw new NotImplementedException(); } }

        public string CollectionName { get { throw new NotImplementedException(); } }

        public Uri CollectionUri { get { throw new NotImplementedException(); } }

        public string CurrentItemId { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } }

        public ICollection<string> InScopeItemIds { get { throw new NotImplementedException(); } }

        public string SortFacetCategory { get { throw new NotImplementedException(); } }

        public string ViewerState { get { throw new NotImplementedException(); } }

        public event EventHandler CollectionLoadingCompleted;

        public event EventHandler<CollectionErrorEventArgs> CollectionLoadingFailed;

        public event EventHandler<ItemActionEventArgs> ItemActionExecuted;

        public event EventHandler<ItemEventArgs> ItemDoubleClicked;

        public event EventHandler<LinkEventArgs> LinkClicked;

        public event PropertyChangedEventHandler PropertyChanged;

        public PivotItem GetItem(string id) { throw new NotImplementedException(); }

        public void LoadCollection(string collectionUri, string viewerState) { throw new NotImplementedException(); }
    }
}

OK, I know, I know - this is pretty convoluted. But, what we now have is a 'real' Silverlight control that exposes the same interface as Microsoft's control. Here's how to use it...

Let's add a new "Silverlight Application" project to our solution - we'll call it PivotViewerApp. For this example we don't need to "Host the Silverlight application in a new Web site". Now add three references to this project (2 of which we already added to our FakePivot project):

  1. System.Windows.Pivot
  2. System.Windows.Pivot.SharedUI
  3. Our FakePivot project

VS2010 should have opened the MainPage.xaml file in "split view" mode. Let's add some references to our 2 namespaces of interest (pivot and fakepivot) and then create our pivot control...

<UserControl x:Class="PivotViewerApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"             
    xmlns:pivot="clr-namespace:System.Windows.Pivot;assembly=System.Windows.Pivot"
    xmlns:fakepivot="clr-namespace:FakePivot;assembly=FakePivot"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <pivot:PivotViewer x:Name="pivotViewer1" />
    </Grid>
</UserControl>

If you now right click on MainPage.xaml in the Solution Explorer and choose "Open in Expression Blend" you'll see the exception I mentioned at the start of this post. So let's go back to VS2010 and change the namespace of our pivotViewer1 control from pivot to fakepivot...


<UserControl x:Class="PivotViewerApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"             
    xmlns:pivot="clr-namespace:System.Windows.Pivot;assembly=System.Windows.Pivot"
    xmlns:fakepivot="clr-namespace:FakePivot;assembly=FakePivot"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <Grid x:Name="LayoutRoot" Background="White">
        <fakepivot:PivotViewer x:Name="pivotViewer1" />
    </Grid>
</UserControl>

Because both namespaces contain a PivotViewer class our xaml is perfectly happy and the solution will still compile. Now go ahead and reopen the same file in Blend - ta dah, no exceptions.

The real beauty of this approach, however, becomes evident when working with the fake control in Blend. Because it exposes exactly the same events, methods and properties as the Microsoft control we can use the Blend UI to hook into these. Selecting pivotViewer1 in our Objects and Timeline window we can then click the Events button in the Properties tab and see all the events that we'd expect our real control to expose.

Once we've done your design work in Blend we, obviously, have to revert our fakepivot namespace to pivot before we can build anything useful. One option is to go back to VS2010 to do this. However, if you hide the Design window in Blend and just have the XAML window visible you can change it there and successfully compile it.

Hopefully, that wasn't too complicated and, once you have your FakePivot library, you can reuse it in any related projects. If anyone want me to do a video of this process please let me know in the comments.

Finally, click here to download the example solution for this post.

2 comments: