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 ();
            
        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

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.

Deployed flawlessly.

Operated reliably 24x7x365.

We build solutions to address your individual business objectives with an eye to sustained technology innovation.

We manage the entire lifecycle from 
start to finish to eliminate surprises 
and allow you to focus on your services.

We deploy environments which 
can be managed and maintained for you by our team of experts 24x7x365.

20+ Years of Experience Makes Us a Trusted Partner You Can Count On.

Atmosera backs technology with real humans who deliver experience and unparalleled dedication, resulting in smarter cloud computing investments. We developed a core methodology to ensure we accurately capture your needs and translate them into the best solution possible. This process gives you the peace of mind that your cloud investment will be aligned with the return you seek. We can be counted on to bring our industry experience and real-world best practices to operate Microsoft Cloud Platform and Azure environments.

Assess:

Migrate:

Re-platform:

Operate:

Rely on our team to map your existing environment to a corresponding 
Azure cloud.

Easily move from your existing environment to a public or private Microsoft cloud.

Understand how to transform your applications to better take advantage 
of cloud capabilities.

Our team actively manages all maintenance & optimization to keep your environment running at its best.

Information Security: Protect Your IT with Industry Best Practices.

We provide managed Information Security (InfoSec) and compliance options across the entire computing stack, from connectivity to applications, with stringent physical and logical security controls.

We take on the security and infrastructure concerns by becoming an extension to our customer’s team or their application development vendor. In that partnership, Atmosera shares the responsibility and liability associated with maintaining a secure environment, and stands by that commitment with a guarantee based on the defined lines of responsibility. All Atmosera customers benefit from more than technology by also getting processes, policies, training, and 24x7x365 technical resources. Customers have the peace of mind of knowing an industry expert has performed a thorough risk assessment, identified a remediation plan and provided ongoing audit support to ensure customers stay secure. Best of all, Atmosera understands this level of service is, and will continue to be, required year after year.

Managed Compliance
Stay compliant with IRS-1075, PCI-DSS, HIPAA/HITECH, and HITRUST.
Disaster Recovery (DR) 
& Data Protection
Safeguard your applications and data and stay operational event when disasters strike.
Secure Networks including Firewalls & DDOS Mitigation
Implement networks which deliver better security and usability.

Why Microsoft Cloud Platform and Azure?

Microsoft has made major strides in the public cloud space over the past few years. They are gaining market momentum with companies and also the analysts community who recognize the more than USD15B investment made. At this point Azure has more data centers than AWS and Google Cloud combined. It spans an unparalleled global reach with 36 regions available and more being added. Azure offers companies wanting to put workload in the a public cloud with a secure, scalable and competitive option.

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.

Azure Certified for Hybrid Cloud.

Azure Certified for Hybrid Cloud.

Atmosera has developed core competencies and intellectual property to take full advantage of all Azure has to offer. All Atmosera Azure services receive the “Azure Certified for Hybrid Cloud” designation which confirms it passed Microsoft’s rigorous validation process under the Cloud OS Network (COSN) program. Customers benefit from knowing that their solution was carefully planned and executed using best practices and proven methods.

Microsoft Azure Government.

Starting in June of 2017, Microsoft opened up their Microsoft Azure government regions to partners. Atmosera was one of the first Microsoft Cloud Solution Provider (CSP) to be approved to deploy qualified government customers in these dedicated regions.
Azure Government offers world-class security and compliance for US federal, state, local, and tribal government agencies and their partners. It provides an environment for government-only workloads which is operated by screened US citizens. Customers can choose from six data center regions with DoD Impact Level 5 Provisional Authority (PA) including two dedicated regions for US Department of Defense workloads. Environments can leverage hybrid flexibility by maintaining some data and functionality on-premises. Azure Government also offers the most certifications of any cloud provider to simplify critical government compliance requirements.

Azure Government regions where Atmosera can deploy customers include the following six locations:

  • US Gov Virginia
  • US Gov Indiana
  • US Gov Arizona
  • US Gov Texas
  • US DOD East
  • US DOD Central
Atmosera has seen a growing desire to use Azure by government agencies and the availability of Azure Government provides a highly secure and trustworthy cloud where workloads can be deployed and managed. The flexibility of hybrid environments makes it possible to transition workloads to the cloud as slowly or fast as desired by customers. Every deployment includes financially-backed Service Level Agreements or SLAs.

Managed Microsoft Cloud Platform and Azure Solutions for Government Agencies

We deliver solutions that accelerate the value of Azure.

Ready to experience the full power of Microsoft Azure?

Start Today

Blog Home

Stay Connected

Upcoming Events

All Events