Thursday, September 9, 2010

Design-Time Friendly ViewModels with MEF

The Managed Extensibility Framework (MEF) is a very powerful tool for building modular, extensible Silverlight applications. If you've followed this blog, you'll know that it is not just for applications that anticipate plug-ins, but can be used as an inversion of control container, can facilitate region management, and much more. In preparing the material for my upcoming presentation that is an Introduction to MVVM, I decided to take an existing, publicly available Silverlight application and refactor it to use MVVM.

Everyone seems to like Twitter feeds and RSS Readers, so I chose John Papa's example that demonstrates RSS syndication and isolated storage to refactor. It is an excellent little demo and of course for the article the focus was the syndication and isolated storage, not MVVM.

I'm not going to include the full refactor here - I'll discuss it at the event and then post the code later. What I do want to touch upon, however, is a common issue that people run into when using MEF in conjuction with the Model-View-ViewModel pattern: design-time compatibility. Because the built-in designer for Blend and VS 2010 ("Cider" is the name for the VS 2010 flavor) doesn't actually use the Silverlight runtime, your controls are run in a different CLR than the target application. The result is that MEF compositions fail, which means controls that rely on MEF ultimately don't get rendered in the designer.

Here is our before picture: you can clearly see what the application is going to look like, but there is no data so it's not clear how the data will fit into the control:

MEF Design-Time Before

There are really just a few easy steps to making MEF views design-time friendly. Let's walk through it.

Step One: Create Your ViewModel

This is straightforward and part of the MVVM pattern. Right now, we'll not worry about design-time as much as having a robust ViewModel to use. For my example, I went with a base ViewModel based on the Prism 4.0 drops. The ViewModel itself ended up looking like this:

[Export]
public class FeedsViewModel : BaseViewModel, IPartImportsSatisfiedNotification
{        
    private const string ERROR_INVALID_URI = "Invalid Uri.";
    private const string ERROR_DUPLICATE_FEED = "Duplicate feed not allowed.";

    private bool _add;

    [Import]
    public IFeedStore FeedStore { get; set; }

    [Import]
    public IFeedService FeedService { get; set; }

    public FeedsViewModel()
    {
        Feeds = new ObservableCollection<SyndicationFeed>();           

        AddFeedCommand = new DelegateCommand<object>(
            obj =>
                {
                    _ValidateNewFeed();

                    if (!AddFeedCommand.CanExecute(obj)) return;

                    _add = true;
                    _AddFeed(new Uri(_newFeed, UriKind.Absolute));
                    NewFeed = string.Empty;
                },
            obj => !HasErrors
            );

        Feeds.CollectionChanged += (o, e) =>
                                        {
                                            RefreshFeedCommand.RaiseCanExecuteChanged();
                                            RaisePropertyChanged(() => Items);
                                        };

        RefreshFeedCommand = new DelegateCommand<object>(
            obj =>
                {
                    Feeds.Clear();
                    _RefreshFeeds();
                },
            obj => Feeds.Count > 0);
    }

     public DelegateCommand<object> AddFeedCommand { get; private set; }

    public DelegateCommand<object> RefreshFeedCommand { get; private set; }

    public ObservableCollection<SyndicationFeed> Feeds { get; private set; }

    public int Count { get; set; }

    public IEnumerable<SyndicationItemExtra> Items
    {
        get
        {                
            var query =
                from f in Feeds
                from i in f.Items
                orderby i.PublishDate descending
                select new SyndicationItemExtra {FeedTitle = f.Title.Text, Item = i};

            Count = query.Count();
            RaisePropertyChanged(()=>Count);
            return query;
        }
    }

    private string _newFeed;

    public string NewFeed
    {
        get { return _newFeed; }
        set
        {
            _newFeed = value;                

            RaisePropertyChanged(() => NewFeed);
            _ValidateNewFeed();
        }
    }

    private void _ValidateNewFeed()
    {
        Uri testUri;
        if (Uri.TryCreate(_newFeed, UriKind.Absolute, out testUri))
        {
            if ((from feed in Feeds where feed.BaseUri.Equals(testUri) select feed).Count() > 0)
            {
                SetError(() => NewFeed, ERROR_DUPLICATE_FEED);
            }
            else
            {
                ClearErrors(() => NewFeed);
            }
        }
        else
        {
            SetError(() => NewFeed, ERROR_INVALID_URI);
        }

        AddFeedCommand.RaiseCanExecuteChanged();
    }

    private void _SaveFeeds()
    {
        FeedStore.SaveFeeds(Feeds.Select(feedItem => feedItem.BaseUri).AsEnumerable());
    }

    private void _AddFeed(Uri feedUri)
    {
        FeedService.FetchFeed(feedUri, (ex, feed) =>
                                            {
                                                if (ex != null)
                                                {
                                                    SetError(() => NewFeed, ex.Message);
                                                    return;
                                                }

                                                var oldFeed = (from feedEntry in Feeds
                                                                where feedEntry.BaseUri.Equals(feed.BaseUri)
                                                                select feedEntry).FirstOrDefault();

                                                if (oldFeed != null)
                                                {
                                                    Feeds.Remove(oldFeed);
                                                }

                                                Feeds.Add(feed);

                                                if (!_add) return;

                                                _add = false;
                                                _SaveFeeds();
                                            });
    }

    private void _RefreshFeeds()
    {
        var feedList = new List<SyndicationFeed>(Feeds);
            
        if (feedList.Count == 0)
        {
            FeedStore.LoadFeeds(list=>
                                    {
                                        foreach(var feed in list)
                                        {
                                            _AddFeed(feed);
                                        }
                                    });
            return;
        }

        foreach (var feed in feedList)
        {
            _AddFeed(feed.BaseUri);
        }
    }

    public void OnImportsSatisfied()
    {
        _RefreshFeeds();
    }
}

As you can see, this ViewModel relies on MEF to compose many of its parts. I've pulled out the saving of the feed list to an external service so I can tweak it as needed, and I've also abstracted the call to the syndication service. I've exposed properties and commands and use a query to aggregate the feeds together. This is a lot of functionality and without the required service and storage dependencies, breaks down in the designer. That's OK, there is hope ...

Step Two (Optional): Define an Interface

This might be step one, actually, it all depends on how you work. I like to get my ViewModel working fine, then define the interface and keep up with it. The only purpose of the interface here is to make it easier to define a design-time ViewModel. If you use a tool like JetBrains ReSharper, it's as easy as right-clicking, choosing "Refactor" and then "Extract Interface." We end up with this:

public interface IFeedsViewModel
{
    DelegateCommand<object> AddFeedCommand { get; }

    DelegateCommand<object> RefreshFeedCommand { get; }

    ObservableCollection<SyndicationFeed> Feeds { get; }

    int Count { get; set; }

    IEnumerable<SyndicationItemExtra> Items { get; }

    string NewFeed { get; set; }
}

Step Three: Create a Design-Time ViewModel

Now that we have an interface, we can implement it in another ViewModel we create specifically for runtime. This ViewModel can create new instances of collections and wire in sample data for us. In our example, I've done this:

public class DesignFeedsViewModel : IFeedsViewModel 
{
    private const string DESIGN_FEED = @"http://feeds.feedburner.com/csharperimage/";
    private const string DESIGN_TITLE = "Test Feed ";
    private const string DESIGN_DESCRIPTION = "A test feed for design-time display";
    private const string LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
    private const string FEED_TITLE = "Feed ";

    public DesignFeedsViewModel()
    {
        Count = 10;
        NewFeed = DESIGN_FEED;

        Feeds = new ObservableCollection<SyndicationFeed>();

        for (var x = 0; x < 3; x++)
        {
            Feeds.Add(new SyndicationFeed(DESIGN_TITLE + x, DESIGN_DESCRIPTION, new Uri(DESIGN_FEED)));
        }

        var designItems = new List<SyndicationItemExtra>();
            
        for (var x = 0; x < 10; x++)
        {
            var item = new SyndicationItem(LOREM, string.Empty,
                                            new Uri(DESIGN_FEED, UriKind.Absolute));
            designItems.Add(new SyndicationItemExtra { FeedTitle = FEED_TITLE + x, Item = item });
        }

        Items = designItems;
    }

    public DelegateCommand<object> AddFeedCommand
    {
        get { return new DelegateCommand<object>(); }
    }

    public DelegateCommand<object> RefreshFeedCommand
    {
        get { return new DelegateCommand<object>(); }
    }

    public ObservableCollection<SyndicationFeed> Feeds { get; set; }
        
    public int Count { get; set; }
        
    public IEnumerable<SyndicationItemExtra> Items { get; set; }

    public string NewFeed { get; set; }        
}

Now, a quick note: at this point, you probably realize you could have gotten away with just one ViewModel. In that case, you'd do something like this:

if (DesignerProperties.IsInDesignTool)
{
   // code for design-time
   return;
}

That's perfectly fine but does a lot of mixing of code ... I've grown to prefer keeping my design-time view models separate and synchronizing them with the interface. It's totally up to you!

Step Four: Bind Your Production ViewModel

This is a key step. If you are directly binding the ViewModel in XAML, and using CompositionInitializer to fire up MEF, you'll need to wrap a condition around it so it doesn't fire that command in design time. There are many ways to bind the ViewModel to the view. I've written about a few:

The bottom line is you can bind it however it makes sense. For simple solutions, this is a pattern I've come to enjoy. While it does involve code-behind, it cleanly separates the MEF ViewModel from design-time because it is not invoked in the constructor (this only works if you export the View as well):

[Export("RootVisual",typeof(UserControl))]
public partial class Reader : IPartImportsSatisfiedNotification
{                
    [Import]
    public FeedsViewModel ViewModel { get; set; }

    public Reader()
    {
        InitializeComponent();
    }       

    public void OnImportsSatisfied()
    {
        LayoutRoot.DataContext = ViewModel;
    }
}

Here, I'm taking advantage of the interface that MEF uses when it wires up a class. Once all dependencies are resolved, it will call OnImportsSatisfied and I can glue my ViewModel. In the designer, the control is simply created using new() so there is no MEF call. So how do we get our design-time data?

Step Five: Bind Your Design-Time ViewModel

Binding the design-time ViewModel is actually very straightforward, especially with the help of design-time extensions. At the top of our XAML, we'll two references: one for the design-time extensions if they aren't already there, and one for the location of the design-time ViewModel:

<UserControl
    xmlns:design="clr-namespace:SilverlightSyndication.DesignTime"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    .../>

Next, on the root grid where the ViewModel should be bound, we take advantage of the d:DataContext and d:DesignInstance extensions:

<Grid d:DataContext="{d:DesignInstance design:DesignFeedsViewModel, IsDesignTimeCreatable=True}" ...>

Here we are defining a data context that is only valid at design time. We bind it to a "design instance" of our view model we created specifically for the designer.

Now, when we run the application, MEF finds and binds the production ViewModel for us. However, in the designer, the designer finds and binds the design-time view model. This gives us plenty of rich data to work with and keeps the designer so happy they're likely to buy you a steak dinner (that last was just in case any of the designers I work with are reading this).

With a little Toolkit Theme love, a rebuild, and a refresh of the XAML in the designer, I now get this:

MEF Design-Time After

Jeremy Likness

2 comments:

  1. This is exactly what I need to know today! Thank you very much.

    ReplyDelete