Migrate from Basic to MVVM and MEF Composable Patterns for a Silverlight Application - Part 1






4.67/5 (3 votes)
The article series shows how to upgrade a Silverlight application having basic patterns to the MVVM and MEF composable patterns with easy approaches and detailed coding explanations.
- Download source (Part 1) - 431 KB
- Download source (Part 2) - 474 KB
- Download source (Part 3/complete) - 509 KB
Introduction
One of my previous posts shows a simple demo application that uses a WCF RIA Services class library with the code first domain data service for CRUD data operations. There are a main screen and a child window with basic navigation and code-behind patterns. What happens if we upgrade the application to that with the MVVM and MEF composable patterns? How easy are the approaches? What are the details of the coding? The article series will address the questions with the easiest approaches and detailed coding explanations. The application, after completed, will not be a full-fledged sample, but should include all major aspects regarding the MVVM and MEF composable pattern implementation without focusing on some other areas such as the UI, data validations, data service operations, or security. I'll also describe how to handle the popup child window with the new patterns, perform the composable part clean-up, and persist the state when switching screens in pure MVVM styles.
Contents
- Part 1 - Start the Work on Pattern Changes
- Architecture Briefs
- Creating the Main Content Holder Project
- Restructuring the Existing ProductApp Project
- Adding the Common Class Library Project
- Making ProductList Class Exportable
- Loading the Xap and Exported Module
- Part 2 - Convert to the Composable MVVM
- Part 3 - Extend the Application
Architecture Briefs
The existing demo application has a simple structure. Classes are directly referenced and all business logic processing code pieces are in the code-behind partial classes.
When the application is updated to the MVVM and MEF composable patterns, the composable parts (also referred to as modules in the article series) are added and existing parts are also re-structured to the modules as shown below.
The diagram indicates the following features.
- The View in the ProductApp.Main.xap is set as the main content holder.
- The xap assembly can have other sets of MVVM (Another Screen for this case) for functionality related to application initiation, such as an authentication process.
- Other multiple xap files can dynamically be exported and the Views can be selectively displayed in the main content holder.
- ViewModels and Models other than in ProductApp.Main xap are in separate assemblies/projects.
- It is easy to add any assembly/project and module for extending the application.
- Only essential parts of the MVVMLight library are used for the commanding, messaging, and module clean-up. Any complex add-in framework is avoided for easy learning and implementation.
Creating the Main Content Holder Project
When opening the existing ProductApp application in the Visual Studio, the Silverlight server and client projects in the solution are shown as below.
The existing application on the client side starts directly from the ProductApp project whereas this project will be a composable xap in the new pattern design. We'll create another client project, ProductApp.Main, which can operate as a main content holder (or switch board). It can also host some modules running on the application starting phase. We would like to re-use the existing ProductApp client project with modifications so that all existing references and some needed items can be carried over to the new project.
Export a template of the existing project by select Export Template… from the File menu and then select the ProductApp project from the dropdown list. Use all other default selections on the Export Template Wizard screens to finish the task. See this document for details of exporing a custom template.
Add the new project ProductApp.Main into the solution with the standard steps but using the custom template ProductApp.
Go to the web host server project ProductApp.Web. On the Silverlight Application section of the project Properties screen, click Add and then, on the Add Silverlight Application screen, select the ProductApp.Main from the dropdown list. Leave the Add a test page that references the control box checked since we need the starting pages for this new project.
Go back to the ProductApp.Main project in the Solution Explorer. Delete the AddProductWindow.xaml and ProductList.xaml files that were carried over from the custom template. Then drag and drop the ErrorWindow.xaml to the ProductApp.Main project root. The general error display screen is more related to the UI and will not be changed to the MVVM and MEF composable patterns.
Add a new Silverlight User Control named MainPage.xaml into the Views folder of the ProductApp.Main project. Now the folder and file structure of the project looks like this.
Open the App.xaml.cs file and change the
ProjectList
toMainPage
in theApplication_Start()
.We'll add other code pieces into the files in the ProductApp.Main project later.
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new Views.MainPage();
}
Restructuring the Existing ProductApp Project
The ProductApp project will be renamed to ProductApp.Views which operates as a composable xap. Follow the renaming approaches to complete the changes.
Drag the ProductList.xaml and AddProductWindow.xaml from the Views folder and drop them to the project root. We don’t need the Views folder to group the files since this project will only hold files for Views.
Delete the Views folder with the ErrorWindow.xaml file still in it. We also don’t need the built-in error reporting window in this project.
The initiating
App
class (App.xaml and App.xaml.cs) and the Styles.xaml in the Assets folder are actually not used at the runtime by the ProductApp.Views.xap as a composable part to be executed within the ProductApp.Main context. Deleting these files in the project will not affect the application behaviors at the runtime. But we may keep them there for the use in the design time or if testing the assembly by initiating it directly. In this case we need to replace the methods for rendering the unhandled exceptions in the App.xaml.cs file due to the removal of the ErrorWindow.xaml. Otherwise, we’ll get a compile error. Because of the low significance, the updated code is not shown here. You can copy the code or even the App.xaml and its .cs files from the downloaded source package.In the web host server project, ProductApp.Web, delete the ProductAppTestPage.aspx and ProductAppTestPage.html pages. The application will start from the new test page. The restructured ProductApp.Views client project and the web host server project should look like this.
Save all updated files, set the ProductAppTestPage.aspx as the starting page, and run the application. The Product List screen in the ProductApp.Views project should be shown as the same as before.
Note that since all files, except the App.xaml and App.xaml.cs, already have the namespace ProductApp.Views, we should only replace the namespace ProductApp with ProductApp.Views for the App.xaml and App.xaml.cs files by looking in the Current Document. Also make sure that the xap file name is replaced with ProductApp.Views.xap.
Adding the Common Class Library Project
The basic operations using MEF are performed or mediated by the code in this project.
Add a new Silverlight class library project with the name of ProductApp.Common into the solution.
Add these references into the project:
- ProductRiaLib (project)
- System.ComponentModel.Composition (.NET)
- System.ComponentModel.Composition.Initialization (.NET)
- System.ServiceModel.DomainServices.Client (.NET)
Delete the auto-generated Class1.cs file and then create the Constants and ModuleServices folders in the project. Folders are merely used to group related files for clarity and easy maintenance. Any classes and interfaces in the project are under the default root namespace without their folder names added.
Add a new class file named ModuleID.cs into the Constants folder.
Add three new class files, IModule.cs, IModuleMetadata.cs, and ModuleCatelogSerive.cs into the ModuleServices folder. Now the ProductApp.Common project in the Solution Explorer should look like this.
Open the Contants\ModuleID.cs file and replace everything with the code lines below. We define only one module ID this time for accessing the ProductListView module in the exported ProductApp.Views.xap.
Add the code to the ModuleServices\IModule.cs and replace anything existing. It’s the simplest Interface that serves as the module type and export/import contractor.
Enter the code lines into the ModuleServices\IModuleMetadata.cs and replace any existing code. We just need one common metadata property, the
Name
, for the MEF export.Enter the following code lines into the ModuleServices\ModuleCategoryService.cs and override any existing code. Members of this class perform the core MEF functionality for the application. The comment labels explain what the code lines exactly do. We also place all members in the class this time although some will not be called until we implement processes described in subsequent parts of the article series.
Add reference of this project into all Silverlight client projects. At present, only the ProductApp.Main and ProductApp.Views are involved.
Save all updated files in this project. We are ready for loading the ProductApp.Views.xap and exporting the exiting ProductList module from the xap for display.
namespace ProductApp.Common
{
public sealed class ModuleID
{
// Product List
public const string ProductListView = "ProductListView";
}
}
namespace ProductApp.Common
{
// Just used as a common type for any exported module.
public interface IModule
{
}
}
namespace ProductApp.Common
{
// A metadata view containing only one property
public interface IModuleMetadata
{
string Name { get; }
}
// Use string Constants as keys for metadata properties
// Not enter string values on the spot
public sealed class MetadataKeys
{
public const string Name = "Name";
}
}
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
namespace ProductApp.Common
{
public class ModuleCatalogService
{
// Define a catalog of the mixed type
private static AggregateCatalog _aggregateCatalog;
// Define the dictionary for loaded xaps
private Dictionary<<string, DeploymentCatalog> _catalogs;
// Define the collections for storing expored modules
private List<ExportLifetimeContext<IModule>> _contextList;
private List<Lazy<IModule>> _lazyList;
// Define the container
public static CompositionContainer Container { get; private set; }
// Expose this service instance
public static ModuleCatalogService Instance { get; private set; }
// Define a collection of ExportFactory object
[ImportMany(AllowRecomposition = true)]
public IEnumerable<ExportFactory<IModule,
IModuleMetadata>> FactoryModules { get; set; }
// Define a collection of Lazy object
[ImportMany(AllowRecomposition = true)]
public IEnumerable<Lazy<IModule, IModuleMetadata>> LazyModules { get; set; }
public ModuleCatalogService()
{
_catalogs = new Dictionary<string, DeploymentCatalog>();
_contextList = new List<ExportLifetimeContext<IModule>>();
_lazyList = new List<Lazy<IModule>>();
// Fill the imports of all parts held by this service instance
CompositionInitializer.SatisfyImports(this);
}
static ModuleCatalogService()
{
_aggregateCatalog = new AggregateCatalog();
//Add an instance of catalog for xap to the Catalog collection
_aggregateCatalog.Catalogs.Add(new DeploymentCatalog());
//Use the aggregate catalog with the container
Container = new CompositionContainer(_aggregateCatalog);
// Initialize the logical composition container
CompositionHost.Initialize(Container);
Instance = new ModuleCatalogService();
}
public static void Initialize()
{
// Any call to this static method will call the static constructor
// the "ModuleCatalogService()"
// It's called firstly from the starting App.xaml.cs
}
public void AddXap(string uri, Action<AsyncCompletedEventArgs> completedAction = null)
{
DeploymentCatalog catalog;
if (!_catalogs.TryGetValue(uri, out catalog))
// Run the code if the xap is not loaded (no data in the dictionary)
{
catalog = new DeploymentCatalog(uri);
// Event handler registered and running for adding the xap
catalog.DownloadCompleted += (s, e) =>
{
if (e.Error == null)
{
// Add the DeploymentCatelog instance to the dictionary.
_catalogs.Add(uri, catalog);
// Add the DeploymentCatelog instance to the Catologs collection
_aggregateCatalog.Catalogs.Add(catalog);
}
else
{
throw new Exception(e.Error.Message, e.Error);
}
};
// Set the event handler and notify the caller
if (completedAction != null)
catalog.DownloadCompleted += (s, e) => completedAction(e);
// Begin to download the xap
catalog.DownloadAsync();
}
else
// Xap has been loaded previously and just notify the caller
// to run the event handler routine
{
if (completedAction != null)
{
AsyncCompletedEventArgs e = new AsyncCompletedEventArgs(null, false, null);
completedAction(e);
}
}
}
public void RemoveXap(string uri)
{
DeploymentCatalog catalog;
if (_catalogs.TryGetValue(uri, out catalog))
{
// Remove the DeploymentCatelog instance in the Catalogs collection
_aggregateCatalog.Catalogs.Remove(catalog);
// Remove the xap from the dictionary
_catalogs.Remove(uri);
}
}
public object GetModule(string moduleId)
{
// An object to hole an instance of ExportLifetimeContext
ExportLifetimeContext<IModule>> context;
// Search for the module by matching the metadata Name property
context = FactoryModules.FirstOrDefault(
n => (n.Metadata.Name == moduleId)).CreateExport();
// Cache the instance of ExportLifetimeContext to the list
_contextList.Add(context);
return context.Value;
}
public object GetModuleLazy(string moduleId)
{
// A Lazy object to hold the exported module
Lazy<IModule, IModuleMetadata> lazyModule;
// Search for the module by matching the metadata Name property
lazyModule = LazyModules.FirstOrDefault(n => n.Metadata.Name == moduleId);
// Cache the instance of the Lazy to the list
_lazyList.Add(lazyModule);
return lazyModule.Value;
}
public bool ReleaseModule(IModule module)
{
// Set module reference back to the context
ExportLifetimeContext<IModule> context =
_contextList.FirstOrDefault(n => n.Value.Equals(module));
if (context == null) return false;
// Remove the module context from the collection
_contextList.Remove(context);
// Calls Cleanup() in the module and then set the module null.
context.Dispose();
context = null;
return true;
}
public bool ReleaseModuleLazy(IModule module)
{
// Set module reference back to the lazyModule
Lazy<IModule> lazyModule = _lazyList.FirstOrDefault(n => n.Value.Equals(module));
if (lazyModule == null) return false;
// Remove the module lazy from the collection
_lazyList.Remove(lazyModule);
// No Dispose() for the Lazy object and call this method of the container
Container.ReleaseExport(lazyModule);
lazyModule = null;
return true;
}
}
}
Making ProductList Class Exportable
We need to add the Export
attributes to the ProductList class to make it composable. Please refer to the MSDN document for details
of using Export/Import attributes. We here use the common type IModule
as the contract
and only metadata Name
property with the module ID as its value. The module ID is essential for the communications between export and import parties.
We do not use the custom attributes to avoid creating more interfaces or classes.
Add the System.ComponentModel.Composition (.NET) reference into the ProductApp.Views project.
Open the ProductList.xaml.cs code-behind. Add or resolve the two lines of using statements. Then replace the code lines from the namespace line to the class definition line as shown below.
// - - - Existing using statement lines above
using System.ComponentModel.Composition;
using ProductApp.Common;
namespace ProductApp.Views
{
[Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, ModuleID.ProductListView)]
public partial class ProductList : UserControl, IModule
{
// - - - other existing code below
Loading the Xap and Exported Module
We can now modify the ProductApp.Main project to obtain the ProductApp.Views.xap and the import the ProductList module.
Open the ProductApp.Main/App.xaml.cs file and add the code to initialize the catalog service. This will call the constructors of the
ModuleCategoryService
for loading the objects for processing the exporting/importing and creating the composition container.Replace the existing code in the MainPage.xaml file with the following code lines. This creates a screen with the application band, a link button on the band, and a simple content control. We keep the navigation UI as simple as possible here. For a real world application, any other element and style such as menu or tab controls can be used on the MainPage user control.
Replace the existing code in the MainPage.xaml.cs code-behind with the following code snippet. At this time, we still use the code-behind to load the xap file and import the module. We'll update the application to the composable MVVM patterns in the next part of the article series.
Run the application. The ProductList module from the loaded ProductApp.Views.xap should be exported into the composition container and displayed on the screen.
private void Application_Startup(object sender, StartupEventArgs e)
{
// Initilizing the catelog service on the application start
ProductApp.Common.ModuleCatalogService.Initialize();
this.RootVisual = new Views.MainPage();
}
<UserControl x:Class="ProductApp.Main.Views.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"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot"
Style="{StaticResource LayoutRootGridStyle}">
<Grid x:Name="NavigationGrid"
Style="{StaticResource NavigationGridStyle}">
<Border x:Name="BrandingBorder"
Style="{StaticResource BrandingBorderStyle}">
<StackPanel x:Name="BrandingStackPanel"
Style="{StaticResource BrandingStackPanelStyle}">
<TextBlock x:Name="ApplicationNameTextBlock"
Style="{StaticResource ApplicationNameStyle}"
Text="Demo Product Application" />
</StackPanel>
</Border>
<Border x:Name="LinksBorder"
Style="{StaticResource LinksBorderStyle}">
<StackPanel x:Name="LinksStackPanel"
Style="{StaticResource LinksStackPanelStyle}">
<HyperlinkButton x:Name="linkButton_ProductList"
Style="{StaticResource LinkStyle}"
Content="Product List"
Click="LinkButton_Click" />
</StackPanel>
</Border>
</Grid>
<ScrollViewer x:Name="PageScrollViewer"
Style="{StaticResource PageScrollViewerStyle}"
Margin="0,41,0,0">
<StackPanel x:Name="ContentStackPanel">
<ContentControl x:Name="MainContent"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" />
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;
using ProductApp.Common;
namespace ProductApp.Main.Views
{
public partial class MainPage : UserControl
{
private ModuleCatalogService _catalogService = ModuleCatalogService.Instance;
public MainPage()
{
InitializeComponent();
}
// Still use code-behind for test MEF before MVVM implementation.
private void LinkButton_Click(object sender, RoutedEventArgs e)
{
// Call to load the xap
string xapUri = "/ClientBin/ProductApp.Views.xap";
_catalogService.AddXap(xapUri, arg => ProductApp_OnXapDownloadCompleted(arg));
}
private void ProductApp_OnXapDownloadCompleted(AsyncCompletedEventArgs e)
{
// Inject the module after loading xap completed
MainContent.Content = _catalogService.GetModule(ModuleID.ProductListView);
// UI - set active link
VisualStateManager.GoToState(linkButton_ProductList, "ActiveLink", true);
}
}
}
Summary
In this part of the article series, we have started the work on changing patterns for the Silverlight demo application. We have proposed the architecture design, performed project structural updates, and completed the code for xap file loading and a module exporting. We'll convert the application to that using the MVVM pattern and create more MEF composable modules with the MVVM structure in the Part 2 of the article series.