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
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
to MainPage
in the Application_Start()
.
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new Views.MainPage();
}
We'll add other code pieces into the files in the ProductApp.Main project later.
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.
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.
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.
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.
namespace ProductApp.Common
{
public sealed class ModuleID
{
public const string ProductListView = "ProductListView";
}
}
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.
namespace ProductApp.Common
{
public interface IModule
{
}
}
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.
namespace ProductApp.Common
{
public interface IModuleMetadata
{
string Name { get; }
}
public sealed class MetadataKeys
{
public const string Name = "Name";
}
}
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.
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
{
private static AggregateCatalog _aggregateCatalog;
private Dictionary<<string, DeploymentCatalog> _catalogs;
private List<ExportLifetimeContext<IModule>> _contextList;
private List<Lazy<IModule>> _lazyList;
public static CompositionContainer Container { get; private set; }
public static ModuleCatalogService Instance { get; private set; }
[ImportMany(AllowRecomposition = true)]
public IEnumerable<ExportFactory<IModule,
IModuleMetadata>> FactoryModules { get; set; }
[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>>();
CompositionInitializer.SatisfyImports(this);
}
static ModuleCatalogService()
{
_aggregateCatalog = new AggregateCatalog();
_aggregateCatalog.Catalogs.Add(new DeploymentCatalog());
Container = new CompositionContainer(_aggregateCatalog);
CompositionHost.Initialize(Container);
Instance = new ModuleCatalogService();
}
public static void Initialize()
{
}
public void AddXap(string uri, Action<AsyncCompletedEventArgs> completedAction = null)
{
DeploymentCatalog catalog;
if (!_catalogs.TryGetValue(uri, out catalog))
{
catalog = new DeploymentCatalog(uri);
catalog.DownloadCompleted += (s, e) =>
{
if (e.Error == null)
{
_catalogs.Add(uri, catalog);
_aggregateCatalog.Catalogs.Add(catalog);
}
else
{
throw new Exception(e.Error.Message, e.Error);
}
};
if (completedAction != null)
catalog.DownloadCompleted += (s, e) => completedAction(e);
catalog.DownloadAsync();
}
else
{
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))
{
_aggregateCatalog.Catalogs.Remove(catalog);
_catalogs.Remove(uri);
}
}
public object GetModule(string moduleId)
{
ExportLifetimeContext<IModule>> context;
context = FactoryModules.FirstOrDefault(
n => (n.Metadata.Name == moduleId)).CreateExport();
_contextList.Add(context);
return context.Value;
}
public object GetModuleLazy(string moduleId)
{
Lazy<IModule, IModuleMetadata> lazyModule;
lazyModule = LazyModules.FirstOrDefault(n => n.Metadata.Name == moduleId);
_lazyList.Add(lazyModule);
return lazyModule.Value;
}
public bool ReleaseModule(IModule module)
{
ExportLifetimeContext<IModule> context =
_contextList.FirstOrDefault(n => n.Value.Equals(module));
if (context == null) return false;
_contextList.Remove(context);
context.Dispose();
context = null;
return true;
}
public bool ReleaseModuleLazy(IModule module)
{
Lazy<IModule> lazyModule = _lazyList.FirstOrDefault(n => n.Value.Equals(module));
if (lazyModule == null) return false;
_lazyList.Remove(lazyModule);
Container.ReleaseExport(lazyModule);
lazyModule = null;
return true;
}
}
}
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.
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.
using System.ComponentModel.Composition;
using ProductApp.Common;
namespace ProductApp.Views
{
[Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, ModuleID.ProductListView)]
public partial class ProductList : UserControl, IModule
{
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.
private void Application_Startup(object sender, StartupEventArgs e)
{
ProductApp.Common.ModuleCatalogService.Initialize();
this.RootVisual = new Views.MainPage();
}
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.
<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>
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.
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();
}
private void LinkButton_Click(object sender, RoutedEventArgs e)
{
string xapUri = "/ClientBin/ProductApp.Views.xap";
_catalogService.AddXap(xapUri, arg => ProductApp_OnXapDownloadCompleted(arg));
}
private void ProductApp_OnXapDownloadCompleted(AsyncCompletedEventArgs e)
{
MainContent.Content = _catalogService.GetModule(ModuleID.ProductListView);
VisualStateManager.GoToState(linkButton_ProductList, "ActiveLink", true);
}
}
}
Run the application. The ProductList module from the loaded ProductApp.Views.xap should be exported into the composition container and displayed on the screen.
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.