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:
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 (); 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:
- Yet Another MVVM Locator Pattern
- Transactions in MVVM with View Locator
- X-Fit Reference Application with ViewModel Routing
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:
Atmosera is an Oregon-based company who architects, deploys, and operates public, private, and hybrid Microsoft Cloud Platform and Azure environments for customers across the globe and diverse industries. Since 2011, Atmosera has been a trusted cloud partner to the State of Oregon. State agencies including the Oregon State Hospital (OSH), Oregon Health Authority (OHA), the Department of Justice, the Department of Human Services, and the Department of Treasury host their applications with Atmosera. This includes a partnership with Enterprise Technology Services (ETS) and the State of Oregon Data Center.
The Right Solution for Your Needs.
We deliver a hybrid spectrum of Microsoft Cloud Platform and Azure solutions to government agencies and application developers who demand a modern, open and flexible cloud service platform. We offer trusted, transparent, and secure Infrastructure as a Service (IaaS) and Platform as a Service (PaaS) solutions for production business applications, Business Intelligence (BI), continuous data protection, application availability, test/development, and Software as a Service (SaaS)
Architected to meet your needs.
We build solutions to address your individual business objectives with an eye to sustained technology innovation.
Deployed flawlessly.
We manage the entire lifecycle from start to finish to eliminate surprises and allow you to focus on your services.
Operated reliably 24x7x365.
We deploy environments which can be managed and maintained for you by our team of experts 24x7x365.
The Power of Microsoft’s Hybrid Cloud Platform.
Microsoft is the only hyperscaler to offer a true hybrid cloud solution with the flexibility to run applications using private or public clouds — and the ability to move between them. Atmosera has the skills and expertise necessary to help architect, deploy, and operate integrated hybrid cloud solutions based on the Microsoft Cloud Platform and microservices. This offers our customers a unique set of capabilities and flexibility when planning out their cloud journey.