Click here to Skip to main content
15,881,882 members
Articles / MEF

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

Rate me:
Please Sign up or sign in to vote.
4.67/5 (3 votes)
3 May 2012CPOL9 min read 24.6K   611   15  
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.

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.

11.png

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.

12.png

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.

13.png

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.

  1. 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.

  2. Add the new project ProductApp.Main into the solution with the standard steps but using the custom template ProductApp.

  3. 14.png

  4. 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.

  5. 15.png

  6. 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.

  7. 16.png

  8. 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.

  9. 17.png

  10. Open the App.xaml.cs file and change the ProjectList to MainPage in the Application_Start().

  11. C#
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        this.RootVisual = new Views.MainPage();
    }
  12. We'll add other code pieces into the files in the ProductApp.Main project later.

Restructuring the Existing ProductApp Project

  1. The ProductApp project will be renamed to ProductApp.Views which operates as a composable xap. Follow the renaming approaches to complete the changes.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 18.png

  8. 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.

  1. Add a new Silverlight class library project with the name of ProductApp.Common into the solution.

  2. Add these references into the project:

    • ProductRiaLib (project)
    • System.ComponentModel.Composition (.NET)
    • System.ComponentModel.Composition.Initialization (.NET)
    • System.ServiceModel.DomainServices.Client (.NET)
  3. 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.

  4. Add a new class file named ModuleID.cs into the Constants folder.

  5. 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.

  6. 19.png

  7. 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.

  8. C#
    namespace ProductApp.Common
    {
        public sealed class ModuleID
        {
            // Product List
            public const string ProductListView = "ProductListView";
        }
    }
  9. 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.

  10. C#
    namespace ProductApp.Common
    {    
        // Just used as a common type for any exported module.
        public interface IModule
        {         
        }
    }
  11. 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.

  12. C#
    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";
        }
    }
  13. 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.

  14. C#
    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;
                }
            }
        }
  15. Add reference of this project into all Silverlight client projects. At present, only the ProductApp.Main and ProductApp.Views are involved.

  16. 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.

  1. Add the System.ComponentModel.Composition (.NET) reference into the ProductApp.Views project.

  2. 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.

  3. C#
    // - - - 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.

  1. 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.

  2. C#
    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();
    }
  3. 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.

  4. XML
    <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>
  5. 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.

  6. C#
    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);
            }
        }
    }
  7. Run the application. The ProductList module from the loaded ProductApp.Views.xap should be exported into the composition container and displayed on the screen.

  8. 20.png

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Shenwei is a software developer and architect, and has been working on business applications using Microsoft and Oracle technologies since 1996. He obtained Microsoft Certified Systems Engineer (MCSE) in 1998 and Microsoft Certified Solution Developer (MCSD) in 1999. He has experience in ASP.NET, C#, Visual Basic, Windows and Web Services, Silverlight, WPF, JavaScript/AJAX, HTML, SQL Server, and Oracle.

Comments and Discussions

 
-- There are no messages in this forum --