Jounce Part 14: Using MEF for Non-Shared Views and View Models

Even if you don’t use Jounce, this post will help you better understand how to create non-shared views and view models with the Managed Extensibility Framework (MEF). The architecture I prefer in Silverlight is to keep a shared view and view model where possible. If only one instance of the view is visible at a time, the view model can be manipulated to provide the correct bindings and information when different data is selected for that view. Keeping a single view avoids issues with waiting for multiple views in garbage collection or keeping track of multiple views that might not unhook from the visual tree correctly.

There are several cases where this is not plausible. In a master/detail scenario with dockable views, you may end up with multiple “detail” views on the screen. Each view requires a separate copy and possibly a separate view model as well.

Lazy Shared Views and View Models

In MEF, the Lazy<T,TMetadata> syntax is used to import multiple implementations of a contract and associated metadata. The pattern allows you to inspect the metadata before creating the object instances, so you have full control over what is happening. Jounce uses this feature to bind views and view models by inspecting the metadata and using that to examine “routes” created by the user (a route simply asserts that view “X” requires view model “Y”).

The view model import looks like this:

[ImportMany(AllowRecomposition = true)]
public Lazy<IViewModel, IExportAsViewModelMetadata>[] ViewModels { get; set; }

The array can be queried with LINQ. In Jounce, the view model metadata simply provides a user-defined tag for the view model so it can be referenced without knowing the type (this is useful, for example, when the view model must be referenced before the XAP that contains it has been loaded). Here is the metadata contract:

public interface IExportAsViewModelMetadata
{
    string ViewModelType { get; }
}

Here is a simple query to get the associated information for the view model. What’s important to note is that part of this information is a Lazy<T> property for the view model. When the value property is accessed, the view model is created, but any subsequent references will use the same copy, or a “shared” view model:

var vmInfo = (from vm in ViewModels
                where vm.Metadata.ViewModelType.Equals(viewModelType)
                select vm).FirstOrDefault();

Using the Export Factory

To get a fresh copy, instead of using the lifetime management attributes that MEF provides, I chose to go with the ExportFactory. Unlike a Lazy import, the export factory does not provide a copy of the target object. Instead, it provides the means of creating a new copy. In other words, you are provided with an actual factory to generate new copies. The convention for this is very similar to the lazy version:

[ImportMany(AllowRecomposition = true)]
public List<ExportFactory<IViewModel, IExportAsViewModelMetadata>> ViewModelFactory { get; set; }

Notice that again metadata exists, so you can inspect the factory to make sure it is the right factory before asking it to generate a new object. Now it is a simple matter to query for the right view model and then ask the export factory to generate a new copy:

public IViewModel GetNonSharedViewModel(string viewModelType)
{
    return (from factory in ViewModelFactory
            where factory.Metadata.ViewModelType.Equals(viewModelType)
            select factory.CreateExport().Value).FirstOrDefault();
}

The Ties that Bind

Now you are able to generate a view and a view model on the fly. Jounce adds helper methods to view models to help synchronize with the view. For example, InitializeViewModel is called the first time the view model is bound to the view, and ActivateView is called when a view is loaded that is bound to the view model. There is also a binding for the visual state manager so that the view model can transition states without being aware of the view.

In order to bind property, Jounce provides a simple mechanism for generating a new copy of a view that allows you to pass in the data context:

UserControl GetNonSharedView(string viewTag, object dataContext);

If you have simple views, this can take a model or other class that is not a view model and will still bind it to the data context for you. However, if the bound object is a Jounce view model, the binding will add some additional calls and hooks to ensure that visual states and other bindings are updated as needed. This is what the view creation method looks like:

public UserControl GetNonSharedView(string viewTag, object dataContext)
{
    var view = (from factory in ViewFactory
                where factory.Metadata.ExportedViewType.Equals(viewTag)
                select factory.CreateExport().Value).FirstOrDefault();

    if (view == null)
    {
        return null;
    }

    _BindViewModel(view, dataContext);
                
    var baseViewModel = dataContext as BaseViewModel;
    if (baseViewModel != null)
    {
        baseViewModel.RegisterVisualState(viewTag,
                (state, transitions) => JounceHelper.ExecuteOnUI(
                  () => VisualStateManager.GoToState(view, state, transitions)));
        baseViewModel.RegisteredViews.Add(viewTag);
        baseViewModel.Initialize();
        RoutedEventHandler loaded = null;
        loaded = (o, e) =>
                        {
                            ((UserControl) o).Loaded -= loaded;
                            baseViewModel.Activate(viewTag, new Dictionary<string, object>());
                        };
        view.Loaded += loaded;
    }
    return view;
}

Notice the use of the variable to allow unbinding the lambda expression after the first loaded event is fired (the load event is triggered every time the control is placed in the visual tree, but the bindings only need to be updated once).

DataTemplate Selectors

The final step for Jounce was to create a special value converter for spinning views. This is similar to the concept I wrote about in Data Template Selector using the Managed Extensibility Framework. Only in this case, the converter will do one of two things: if passed a Jounce view model, it will automatically find the view the view model is associated with, generate a new copy of the view, and bind it, or if passed a parameter, it will create a view with the tag provided in the parameter.

The Jounce quickstart (available by grabbing the latest Jounce source download) contains an example of this. The target model is a contact class:

public class Contact
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
}

The view model for the model simply passes through to the underlying properties. In a full implementation, it would save the orignal model for roll back and have other options such as commands to manipulate the data:

[ExportAsViewModel("ContactVM")]
public partial class ContactViewModel : BaseViewModel
{
    public ContactViewModel()
    {
        if (DesignerProperties.IsInDesignTool)
        {
            SetDesignerData();
        }
    }

    public Contact SourceContact { get; set; }
        
    public string FirstName
    {
        get { return SourceContact.FirstName; }
        set
        {
            SourceContact.FirstName = value;
            RaisePropertyChanged(()=>FirstName);
        }
    }

    public string LastName
    {
        get { return SourceContact.LastName; }
        set
        {
            SourceContact.LastName = value;
            RaisePropertyChanged(()=>LastName);
        }
    }

    public string Address
    {
        get { return SourceContact.Address; }
        set
        {
            SourceContact.Address = value;
            RaisePropertyChanged(()=>Address);
        }
    }

    public string City
    {
        get { return SourceContact.City; }
        set
        {
            SourceContact.City = value;
            RaisePropertyChanged(()=>City);
        }
    }

    public string State
    {
        get { return SourceContact.State; }
        set
        {
            SourceContact.State = value;
            RaisePropertyChanged(()=>State);
        }
    }
}

The view simply shows the last name, first name in bold and then the address on the second line:

<Grid x_Name="LayoutRoot" Background="White" d_DataContext="{d:DesignInstance vm:ContactViewModel, IsDesignTimeCreatable=True}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Margin="5">
        <TextBlock FontWeight="Bold" Text="{Binding LastName}"/>
        <TextBlock FontWeight="Bold" Text=", "/>
        <TextBlock FontWeight="Bold" Text="{Binding FirstName}"/>
    </StackPanel>
    <StackPanel Orientation="Horizontal" Grid.Row="1" Margin="5">
        <TextBlock Text="{Binding Address}"/>
        <TextBlock Text=" "/>
        <TextBlock Text="{Binding City}"/>
        <TextBlock Text=", "/>
        <TextBlock Text="{Binding State}"/>
    </StackPanel>
</Grid>

Here is the view with sample data in the designer:

The view itself is exported in the code-behind, as well as a binding mapping it to the corresponding view model:

[ExportAsView("ContactView")]
public partial class ContactView
{
    public ContactView()
    {
        InitializeComponent();
    }

    [Export]
    public ViewModelRoute Binding
    {
        get { return ViewModelRoute.Create("ContactVM", "ContactView"); }
    }
}

The main view model starts out by providing a list of contacts (this one generates sample data for the sake of the demo):

[ExportAsViewModel("MainVM")]
public class MainViewModel : BaseViewModel
{
    private readonly List<Contact> _sampleData = new List<Contact>
                                            {
                                                new Contact
                                                    {
                                                        FirstName = "Jeremy",
                                                        LastName = "Likness",
                                                        Address = "1212 Hollywood Blvd",
                                                        City = "Hollywood",
                                                        State = "California"
                                                    },
                                                new Contact
                                                    {
                                                        FirstName = "John",
                                                        LastName = "Doe",
                                                        Address = "12 Driving Parkway",
                                                        City = "St. Petersburg",
                                                        State = "Florida"
                                                    },
                                                new Contact
                                                    {
                                                        FirstName = "Jane",
                                                        LastName = "Doe",
                                                        Address = "1414 Disk Drive",
                                                        City = "Lead",
                                                        State = "South Dakota"
                                                    },
                                                new Contact
                                                    {
                                                        FirstName = "Sam",
                                                        LastName = "Iam",
                                                        Address = "12 Many Terrace",
                                                        City = "Figment",
                                                        State = "Imagination"
                                                    },

                                            };

    public MainViewModel()
    {
        Contacts = new ObservableCollection<Contact>(_sampleData);
    }

    public ObservableCollection<Contact> Contacts { get; private set; }
}

The problem is that our view requires a view model (it might handle edits or deletes in the future, so the model itself isn’t enough). No problem! First, create a set of extension methods: one to pass the contact into a view model, and another to handle a list of contacts that will return a list of contact view models:

public static ContactViewModel ToViewModel(this Contact contact, IViewModelRouter router)
{
    var vm = router.GetNonSharedViewModel("ContactVM") as ContactViewModel;

    if (vm == null)
    {
        throw new Exception("Couldn't create view model for contact.");
    }

    vm.SourceContact = contact;
    return vm;
}

public static IEnumerable<ContactViewModel> ToViewModels(this IEnumerable<Contact> contacts, IViewModelRouter router)
{
    return contacts.Select(contact => contact.ToViewModel(router)).ToList();
}

Notice the router is used to get a non-shared copy of the view model, then the contact is passed in. The list function simply applies the conversion to the entire list. Of course, the view model tag can be specified as a constant to avoid the “magic string” and make it easier to refactor down the road.

The main view model can now be tweaked to expose a list of view models:

public IEnumerable<ContactViewModel> ViewModels
{
    get { return Contacts.ToViewModels(Router); }
}

It’s a very simple conversion using the helper extension methods. To ensure the enumerable is re-loaded anytime the underlying collection changes, simply add this after the observable collection is created:

Contacts.CollectionChanged += (o, e) => RaisePropertyChanged(() => ViewModels);

Next the XAML is updated to use the special converter:

<ListBox ItemsSource="{Binding ViewModels}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <ContentControl Content="{Binding Converter={StaticResource ViewConverter}}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

In this example, the binding is a list of contact view models. The special converter will inspect the binding, figure out the view model and find the corresponding view (the contact view we showed earlier), then create a new copy of the view and bind the view model. This could also have been done directly with the contact model if a view model wasn’t being used. More importantly, if the list is type IViewModel it can contain different types of view models, and the converter will automatically find the appropriate view – this is a “View Model First” approach to binding but serves as a data template selector.

When the application is run, the non-shared views and view models spin up and bind as expected:

The full source code for Jounce and the example shown here is available from the Jounce CodePlex site.

Jeremy Likness

Work at the Speed of Ideas in Azure.

We are not short on ideas and increasingly businesses want to implement them as quickly as possible.
This is putting a burden on development teams to accelerate development without sacrificing quality.

We developed a best of breed platform designed specifically for Azure to enable end-to-end automation and testing.
Our service accelerates the deployment of e-commerce sites, corporate websites and portals, and mobile web applications.
Customers get to improve time-to-revenue with a reliable, predictable, and repeatable delivery platform.

Button Text

Consistent through stages.

We enable your teams to work in a logical and linear progression from inception to reality.

Version and release control.

The platform is designed to facilitate a continuous delivery cycle using Agile methodologies.

Accelerate time-to-market.

You will streamline all aspects from identifying problems, rolling back to known good versions, and deploying at anytime of the day.

The Problem with Release Management.

The need to release faster and better quality is growing quickly. The inability to keep up with changing demand can be especially challenging for e-tailers who compete for customer mind-share. The need to move faster and faster results on more possibility of errors. With web development a common symptom of errors are broken links resulting from:

  • Renaming or moving a webpage and forgetting to change the internal link
  • Linking to content (documents, videos, forms, etc.) that has been moved or deleted
  • Linking to external pages where the URL changed or the page moved

Broken links impact Search Engine Optimization (SEO) which can easily reduce traffic to a site. More importantly, broken links have a direct impact on reputation, customer confidence, and completing a transaction. Broken links are a simple example of a very visible break down in testing and proper release management processes.

This is where automation and testing can become a catalyst to business but it is not as easy as it looks:

  • The tools to improve release management are complex, multi-origin, and rapidly changing
  • Not all are platform certified for public clouds such as Azure and can be unwieldy to deploy

As a result, many cut corners – they skip automation and don’t leverage testing, sometimes forgoing it altogether.

Request a Conversation

Put an End to Release Headaches.

We built this service for companies, e-tailers, and web development agencies who demand a better solution to ensure rapid development of content and features. It is ideally suited for automating the release and testing process for a Content Management System (CMS). Users benefit from the ability to:

  • Develop new features and capabilities faster and ensure they will not break the site or application as code gets promoted:
    • a:1:{i:0;s:3:”yes”;}
  • Reduce manual quality control (QC) processes:
    • a:1:{i:0;s:3:”yes”;}
  • Evolve test coverage over time through an on-going consultative process:
    • a:1:{i:0;s:3:”yes”;}
  • Manage code promotion across distributed locations:
    • a:1:{i:0;s:3:”yes”;}

A Best of Breed Platform for Azure.

The Atmosera Release Management as a Service was developed to address the gap which exists for companies and developers wanting to move quickly through release management while ensuring quality control and repeatable testing procedures. The platform was purpose-built for Azure using productized integration code to seamlessly connect industry-leading products including:

  • Chef: infrastructure automation
  • Bitbucket: web-based projects hosting
  • Terraform: infrastructure as code
  • JIRA: bug tracking and project management

Benefit from a Platform Based on DevOps Practices.

Agile and DevOps are all the rage but is your IT really enabling them properly?

Atmosera Release Management as a Service is a foundational platform on top of which your teams can drive better agility and quality. It was built for developers to ensure a framework where both infrastructure and operation are harmonized to deliver the ability to:

  • Track every change for every version and quickly find the source of a problem within your application
  • Leverage a structure which enables you to easily roll back to previous known good versions.
  • Log application feedback and add it to a backlog which can be prioritized for the planning phase of a future version.
  • Drive Continuous Integration (CI) which means you can deploy your application at any time without fear of losing data, customers, or other services related to your application.
Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X