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.

Tuesday 6 July 2010

Social Network Authorisation Needs to Change

A few weeks ago I took a look at a website that needed my twitter login to work. The nature of the site was overtly read-only so I was happy to grant it access via twitter's OAuth process. Yesterday I took another exploratory look at a Facebook application which requested access to my account. Again, the nature of this application was completely read-only. Both apps were mildly interesting and I'd achieved what I'd set out to do. Done.

Imagine my [surprise | outrage | fury] (you choose!) when I discovered that both apps had posted public comments from my account. WTF!? Both used the familiar template of 'I have just used [appX] to do [functionY]. Go to [urlA] to try it yourself.'.

OK, so nothing malicious in that - but I didn't authorise either of these posts. Facebook does give you the ability to deny an application from posting in your name, but only after you've installed it. If the app posts immediately there's nothing you can do about it.

Now, don't get me wrong, good applications deserve to be blown along on the virtual word-of-mouth jet stream; but, and here's the critical bit:

"It should be my decision to publicise my usage of your site."

At TweetPivot we made a very conscious decision to enable a user to promote our site easily but not to presume that that's what they wanted. If the site's good enough they will, but automatically doing it for them removes any worth from the act.

So, what should happen now?

Well, you have to apply to Twitter if you want your application to be able to use their OAuth process. At that point you are asked whether your application requires read-only or read-write access to users' accounts. When I enter my details into the popup OAuth window I want to be told whether I'm giving write rights to the app and, if that's not acceptable to me, I want to be able to decline that 'write' request. If you want me to try out an application that I know has no reason to write to my account then I need confirmation that you can't.

I would hate to see the Twitter authentication process get as complicated as Facebook's became; but it does need improving. The API that all 3rd-parties hook into has very specific, well defined methods. Developers should have to declare, individually, which ones they need to invoke. For instance, if I gave you read-only access to my account how can I be sure that you haven't just farmed off all my private Direct Messages?

Ultimately, this is going to be bad news for application developers that require integration to social networks. The next time I'm asked to try something like this I might hesitate. The time after that I might decline. Good developers are going to be punished and their great apps ignored by the unacceptable actions of the few.