Click here to Skip to main content
Click here to Skip to main content

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

, 3 May 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
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

After completing the work described in the previous parts of article series, we have created the main content holder project and upgraded the Product List screen with its child window from basic patterns to the MVVM and MEF composable patterns. In this part, we'll add another demo screen into the ProductApp.Main project and create another set of projects in the solution for a new xap assembly so that we can switch screens between exported modules and xap assemblies. We'll then implement the module clean-up processes and add the state persistence feature into the application.

Contents and Links

Adding another Screen into the Main Client Project

This screen provides an example of implementing the composable MVVM in the same project and also in the xap assembly that is not exported. We'll keep the modules of this screen as simple as possible for the demo purposes. Any required module ID string constant is already in the ProductApp.Common\Constants\ModuleID.cs files.

  1. In the ProductApp.Common project, add an Interface file, IOtherModel, to the Model folder. It will be used as a contract of communications between new ViewModels and Models. There is only one string property defined for the demo text.

  2. namespace ProductApp.Common
    {
        public interface IOtherModel 
        {
            string DemoTextData { get; set; }
        }
    }
  3. In the ProductApp.Main project, create a new folder, Models, and then add a new class file, AnotherScreenModel.cs into the folder. Place simple code lines to the class. Note that this time we define the exported module as an example of non-shared one.

  4. using System.ComponentModel.Composition;
    using ProductApp.Common;
    
    namespace ProductApp.Main.Models
    {
        [Export(ModuleID.AnotherScreenModel, typeof(IOtherModel))]
        [PartCreationPolicy(CreationPolicy.NonShared)]
        public class AnotherScreenModel : IOtherModel
        {
            public AnotherScreenModel()
            {
                DemoTextData = "Another Screen in main starting project";
            }       
            
            public string DemoTextData { get; set;}                              
        }                                             
    }
  5. In the same project, add the AnotherScreenViewModel.cs file into the ViewModels folder and place the following code to the class. Note that we define the variables for the Lazy object and its Value separately. The Value property of the Lazy object will be set to the class/module instance whereas the Lazy object, not its Value, will be cleaned up when leaving the screen.

  6. using System;
    using System.ComponentModel.Composition;
    using GalaSoft.MvvmLight;
    using ProductApp.Common;
    
    namespace ProductApp.Main.ViewModels
    {
        [Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, 
                       ModuleID.AnotherScreenViewModel)]
        [PartCreationPolicy(CreationPolicy.NonShared)]
        public class AnotherScreenViewModel : ViewModelBase, IModule
        {
            private Lazy<IOtherModel>_lazyAnotherModel;
            private IOtherModel _anotherModel;
            
            public AnotherScreenViewModel()
            {
                // Import the lazy model module that can be removed later
                _lazyAnotherModel = 
                  ModuleCatalogService.Container.GetExport<IOtherModel>(ModuleID.AnotherScreenModel);
                _anotherModel = _lazyAnotherModel.Value;
                
                // Populate the property with data from the Model
                DemoText = _anotherModel.DemoTextData;
            }
    
            private string _demoText;
            // 
            public string DemoText
            {
                // Property exposed for data binding
                get { return _demoText; }
                set
                {
                    if (!ReferenceEquals(_demoText, value))
                    {
                        _demoText = value;
                        RaisePropertyChanged("DemoText");
                    }
                }
            }                      
        }
    }
  7. Add a new Silverlight User Control with the name of AnotherScreen.xaml into the Views folder of the ProductApp.Main project and then add a TextBlock under the Grid node of the xaml file.

  8. <TextBlock Height="23" HorizontalAlignment="Left" Margin="27,55,0,0"
               Name="textBlock1" Text="{Binding Path=DemoText}"
               VerticalAlignment="Top" Width="479" FontSize="13"
               FontWeight="Bold" />

    In the AnotherScreen.xaml.cs, replace the code with these lines.

    using System.Windows.Controls;
    using System.ComponentModel.Composition;
    using GalaSoft.MvvmLight;
    using ProductApp.Common;
    
    namespace ProductApp.Main.Views
    {
        [Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, ModuleID.AnotherScreenView)]
        public partial class AnotherScreen : UserControl, IModule 
        {
            public AnotherScreen()
            {
                InitializeComponent();
                
                if (!ViewModelBase.IsInDesignModeStatic)
                {
                    // Set the DataContext to the imported ViewModel
                    DataContext = ModuleCatalogService.Instance.GetModule(ModuleID.AnotherScreenViewModel);
                }
            } 
        }
    }

    After doing the list above, the folder and file structure of the ProductApp.Main looks like this.

    31.png

  9. Open the MainPageViewModels.cs and add the following code piece into the switch block of OnLoadModuleCommand method. After receiving the navigation command, the ViewModel sends a message to the View for loading the requested screen.

  10. // Send message back to MainPageView code-behind to load AnotherScreenView
    case ModuleID.AnotherScreenView:
        Messenger.Default.Send(ModuleID.AnotherScreenView, MessageToken.LoadScreenMessage);
        _currentViewText = ModuleID.AnotherScreenView;
        break;
  11. Open the MainPage.xaml.cs and add the following code piece into the switch block of OnLoadScreenMessage method for loading the AnotherScreen.xaml View to the content holder.

  12. case ModuleID.AnotherScreenView:
        newScreen = _catalogService.GetModule(ModuleID.AnotherScreenView);
        break;
  13. Add the second HyperlinkButton under the first one in the MainPage.xaml file.

  14. <Rectangle x:Name="Divider1" Style="{StaticResource DividerStyle}" />
    <HyperlinkButton x:Name="linkButton_AnotherScreen" 
                Style="{StaticResource LinkStyle}" Content="Another Screen"
                Command="{Binding Path=LoadModuleCommand}" 
                CommandParameter="AnotherScreenView" />
  15. Run the application. Click the Another Screen link buttons to open the screen on the browser. Switching between exported modules can now be performed.

  16. 32.png

Adding another Xap into the Application

Having done our previous work, adding projects into the solution for a new xap assembly is not difficult. We'll use the shortcuts to create the xap and make the MVVM modules similar to those for the Another Screen link.

  1. Create a virtual folder, AnotherXap.Client, in the solution.

  2. Before creating the custom templates in the next step, let's set Copy Local to False for almost all references in the existing ProductApp.Views, ProductApp.ViewModels, and ProductApp.Models projects under the ProductApp.Client virtual folder. The ProductApp.Main project loads all referenced dll files to its bin folder which are available for all other client projects in the application so that these other assemblies don't need the same copies in their own local bin folder. This will reduce the size of the xap and its referenced ViewModel/Model assemblies for the faster loading. The advantage of using custom templates is that all the reference settings in an existing project will be carried over to the new projects automatically.

  3. In the ProductApp.Views project, select all references except the ProductApp.Models and ProductApp.ViewModels, right-click to open the Properties panel, and select the False from the dropdown list for the Copy Local item.

    33.png

    Do the same for the ProductApp.ViewModel and ProductApp.Model but select all reference items there.

  4. Export custom templates from these three projects under the existing ProductApp.Client virtual folder and then create three new projects under the AnotherXap.Client virtual folder using the corresponding custom templates. The names of new projects will be AnotherXap.Views, AnotherXap.ViewModels, and AnotherXap.Models, respectively.

  5. Perform the changes in the AnotherXap.Models project.

    • Delete the class file in the root folder of the project.
    • Copy/paste the Models\AnotherScreenModel.cs from the ProductApp.Main project to the current project and rename it to AnotherXapModel.cs.
    • Rename the namespace ProductApp.Main.Models to the AnotherXap.Models in the code.
    • Replace all instances of AnotherScreen with AnotherXap in the current document.
    • Replace the value of DemoTextData property with the "IT'S THE MODULE FORM ANOTHER ZAP" or something else.
  6. Perform the similar changes in the AnotherXap.ViewModels project.

    • Delete the class file in the root folder of the project.
    • Copy/paste the ViewModels\AnotherScreenViewModel.cs from the ProductApp.Main project to the current project and rename it to AnotherXapViewModel.cs.
    • Rename the namespace ProductApp.Main.ViewModels to the AnotherXap.ViewModels in the code.
    • Replace all instances of AnotherScreen with AnotherXap in the current document.
  7. Perform the changes in the AnotherXap.Views project.

    • Delete the xaml and class files except the App.xaml and its .cs in the root folder of the project.
    • Copy/paste the Views\AnotherScreen.xaml file from the ProductApp.Main project to the current project and rename it to AnotherXap.xaml. The code-behind .cs file will automatically be copied and renamed when using the Solution Explorer of the Visual Studio.
    • Rename the namespace ProductApp.Main.Views to the AnotherXap.Views in the code of both .xaml and .cs files.
    • Replace all instances of AnotherScreen with AnotherXap in the code of both .xaml and .cs files.

    The project and files under the AnotherXap.Client virtual folder are shown below.

    34.png

  8. Open the App.xaml.cs and replace the code ProductList that was carried over from the template with the code AnotherXap in the Application_Startup method.

  9. In the AnotherXap.Views project, delete the old references of ProductApp.ViewModels and ProductApp.Models that were carried over from the template. Then add references of the new projects, AnotherXap.ViewModels and AnotherXap.Models into the current project.

  10. Open the MainPageViewModels.cs and add the following code piece into the switch block of OnLoadModuleCommand method.

  11. // Load AnotherXap on-demand
    case ModuleID.AnotherXapView:
        xapUri = "/ClientBin/AnotherXap.Views.xap";
        _catalogService.AddXap(xapUri, arg => AnotherXap_OnXapDownloadCompleted(arg));                                
        break;

    We need another event routine in the class to send a message back to the MainPage.xml.cs code-behind to request for exporting the AnotherXap View after the xap loading is completed.

    private void AnotherXap_OnXapDownloadCompleted(AsyncCompletedEventArgs e)
    {
        // Send message back to View code-behind to load AnotherXap View
        Messenger.Default.Send(ModuleID.AnotherXapView, MessageToken.LoadScreenMessage);
                
        _currentViewText = ModuleID.AnotherXapView;
    }
  12. Open the MainPage.xaml.cs and add the following code piece into the switch block of OnLoadScreenMessage method for loading the AnotherXap View to the content holder.

  13. case ModuleID.AnotherXapView:
        newScreen = _catalogService.GetModule(ModuleID.AnotherXapView);
        break;
  14. Add another HyperlinkButton under the existing ones in the MainPage.xaml file.

  15. <Rectangle x:Name="Divider2" Style="{StaticResource DividerStyle}" />
    <HyperlinkButton x:Name="linkButton_AnotherXap" 
                Style="{StaticResource LinkStyle}" Content="Another Xap"
                Command="{Binding Path=LoadModuleCommand}" 
                CommandParameter="AnotherXapView" />
  16. On the web host server ProductApp.Web project, add the AnotherXap.Views project from the existing dropdown list into the web host server project using the Silverlight Application tab on the project Properties page as we did before for adding the ProductApp.Main. But generating starting test pages are not needed this time.

  17. 35.png

  18. Run the application to test the three link buttons and the screen contents.

  19. 36.png

Clean-up of Non-Shared Modules

One of the noticeable issues on a MEF composable application is the disposing composition containers and exported modules. It causes memory leaks if not handled well. For a single container application like this demo application, disposing the container is not the issue. The container's life cycle is the user session. The shared modules are also kept alive until the user closes the application. We focus on the clean-up tasks for the non-shared modules. Here are the basic rules and workflow.

  • The clean-up of modules that is no longer needed starts from the point where a requested new View module is ready for loading.
  • The clean-up is in the sequence of Model, ViewModel, and View.
  • Any module will set its own members to null and then the module will be cleaned up by a call from the next level module.
  • A module having any child or embedded module will be responsible for cleaning up the subsidiaries first.

In our demo application, we'll place the clean-up code for all modules, except the View and ViewMode for the MainPage (content holder) and the ProductListModel (shared). Due to the similarities, I just show below how to code the clean-up processes for modules related to the Product List screen and for some other modules that need special explanations. The downloaded source package contains the finished code for all modules.

  1. Add the code piece below into the place right after the switch block in the OnLoadScreenMessage method of the MainPage.xaml.cs.

  2. // Set the existing View module as object of ICleanup type 
    var viewToCleanUp = MainContent.Content as ICleanup;
    if (viewToCleanUp != null)
    {
        // Start clean-up by calling Cleanup() in the existing View
        viewToCleanUp.Cleanup();
    
        // Remove the existing View from Category Service – the last step
        _catalogService.ReleaseModule((IModule)MainContent.Content);
    }
  3. Go to the ProductApp.Views project, add the ICleanup into the inheritance list for the ProductList class in the ProductList.xaml.cs. Note that all View code-behind modules should implement the ICleanup interface from the MVVMLight, except the MainPage.xaml.cs.

  4. Add the Cleanup method shown below into the ProductList.xaml.cs code-behind. The code firstly calls the Cleanup method in its child window module and then calls catalog service to release the ProductListViewModel module. The ReleaseModule method in the ModuleCatalogService class automatically calls the Cleanup method in the ViewModel module before it releases the ViewModel. The similar clean-up workflow occurs for the child window modules.

  5. public void Cleanup()
    {
        if (_addProdScreen != null)
        {
            // Call Cleanup() in the child window if opened                
            ((ICleanup)_addProdScreen).Cleanup();
    
            // Remove the child window View Lazy module from Catelog Service
            ModuleCatalogService.Instance.ReleaseModuleLazy((IModule)_addProdScreen);
    
            _addProdScreen = null;
        }
    
        if (DataContext != null)
        {
            // Remove its imported ViewModel in the Category Service
            // The context.Dispose() will also call Cleanup() in released module
            // refrenced from MVVM Light ViewModelBase
            ModuleCatalogService.Instance.ReleaseModule((IModule)DataContext);
    
            DataContext = null;
        }
    
        // Clean up any messages this class registered
        Messenger.Default.Unregister(this);
    }
  6. Add the below Cleanup method into the ProductListViewModel.cs which overrides the same method in the ViewModelBase class from the MVVMLight.

  7. public override void Cleanup()
    {            
        if (_productListModel != null)
        {
            // Unregister all event handling                
            _productListModel.GetCategoryLookupComplete -= ProductListModel_GetCategryComplete;
            _productListModel.GetCategorizedProductsComplete -= ProductListModel_GetCategorizedProductComplete;
            _productListModel.SaveChangesComplete -= ProductListModel_SaveChangesComplete;
    
            // No clean-up for shared Model module, just set instance to null                
            _productListModel = null;
        }
                
        // Set any property to null            
        _categoryItems = null;            
        _productItems = null;            
        _selectedCategory = null;
        _currentProduct = null;
        _comboDefault = null;
    
        // Unregister any message for this ViewModel
        base.Cleanup();
    }
  8. Copy the Cleanup method for the AddProductWindow.xaml.cs and AddProductWindowViewModel.cs from the downloaded source package and add the code into the classes. This will complete the clean-up code for the Product List workflow.

  9. Doing the clean-up for the remaining parts is almost the same. A special note may be worth mentioning for cleaning up the Model modules of Another Screen and Another Xap screens.

  10. Open the Models\IOtherModel.cs from the ProductApp.Common project and add the ICleanup as the inherited interface.

    public interface IOtherModel : ICleanup

    As we did previously for a test, the Model classes inheriting the IOtherModel are non-shared modules. We need to test the clean-up for these modules. The Cleanup method in the AnotherScreenViewModels.cs performs the task for its Model instance _anotherModel. Since the Model is exported as a Lazy object, the real Model context is the Value property of the Lazy object. When calling the Container.ReleaseExport, we need to pass the original Lazy object instance as the parameter.

    public override void Cleanup()
    {
        if (_anotherModel != null)
        {
            // Call Cleanup() in the Model
            _anotherModel.Cleanup();
    
            // Remove imported lazy Model from the Category Service
    	    // The parameter is the Lazy object instance, cannot be the Value of the Lazy
            ModuleCatalogService.Container.ReleaseExport<IOtherModel>(_lazyAnotherModel);
    
            _anotherModel = null;
    	    _lazyAnotherModel = null;
            _demoText = null;
        }
    
        // Unregister any message for this ViewModel
        base.Cleanup();
    }
  11. Set breakpoints inside the Cleanup method in any View or ViewModel, or inside the ModuleCatalogService.ReleaseModule method.  Run the application in the debug mode and then switch between screens. You can see the object and value clean-up processes and results using the Autos, Locals, or Quick Watch windows from the Visual Studio.

Persisting States between Composable MVVM Screens

Users would often ask "Where is my stuff when I come back to my previous screen?" if the state is not maintained for an application. The state data can be stored in the database and retrieved for reloading to the previous screen. But keeping the state data in the application local cache is easy for persisting the state within a user session which is usually enough for the user needs. The object cache can normally be placed in the user authentication context but, for the demo purpose, we'll store the state data in the MainPageViewModel context which is persisted during a user session. We'll also use the messaging approaches to transfer the data with fully decoupled styles in the MVVM environment.

  1. Add a new folder named SaveState into the ProductApp.Common project and then add two new class files, StateCache.cs and StateCacheList.cs into the folder.

  2. 37.png

  3. The code lines in the StateCache.cs file should be like this.

  4. namespace ProductApp.Common
    {
        public class StateCache
        {
            // Accept different types of data in the MemberValue field
            public string ModuleID { get; set; }
            public string MemberName { get; set; }
            public object MemberValue { get; set; } 
        }
    }

    The code in the StateCacheList.cs is also simple. The StateCacheList class with the strong type StateCache only has an empty constructor.

    using System.Collections.Generic;
    
    namespace ProductApp.Common
    {
        public class StateCacheList<T> : List<T> where T : StateCache
        {
            public StateCacheList()
            {
            }       
        }
    }
  5. Open the MainPageViewModel.cs and add the code lines into the place under the private variable declare section and override the constructor. We set up two notification message callback handlers using the MVVMLight for getting and putting the state data.

  6. private StateCacheList<StateCache> _mainStateList;
            
    public MainPageViewModel()
    {            
        // For saving module state info
        Messenger.Default.Register(this, MessageToken.PutStateMessage, 
                                    new Action<StateCacheList<StateCache>>(OnPutStateMessage));
        _mainStateList = new StateCacheList<StateCache>();
    
        // For receiving state info
        Messenger.Default.Register<NotificationMessageAction<StateCacheList<StateCache>>>
                    (this, MessageToken.GetStateMessage, message => OnGetStateMessage(message));
    }
    
    // Get cached state data and set loading property accordingly
    private void OnPutStateMessage(StateCacheList<StateCache> stateList)
    {
        if (stateList != null)
        {
            // Remove all previous items for the calling module
            if (_mainStateList != null)
            {
                _mainStateList.RemoveAll(m => m.ModuleID == stateList[0].ModuleID);
            }
                    
            // Add the state list to main list
            _mainStateList.AddRange(stateList);
                    
            stateList = null;
        }
    }
    
    private void OnGetStateMessage(NotificationMessageAction<StateCacheList<StateCache>> message)
    {
        if (message != null)
        {
            // Retrieve the list for the requester
            StateCacheList<StateCache> stateList = new StateCacheList<StateCache>();
            stateList.AddRange(_mainStateList.Where(w => w.ModuleID == message.Notification));
    
            if (stateList.Count > 0)
            {
                // Send the state list back
                message.Execute(stateList);
                        
                stateList = null;
            }
        }
    }
  7. Open the ProductListViewModel.cs file, add the following code to the constructor for requesting the state data when opening the Product List screen.

  8. // Send callback message to retrieve the state info
    Messenger.Default.Send(new NotificationMessageAction<StateCacheList<StateCache>>
                (ModuleID.ProductListViewModel, OnGetStateMessageCallback),
                MessageToken.GetStateMessage);

    Then add these two methods below the constructor for repopulating the data property after the callback and for saving the state data before close the screen, respectively.

    private void OnGetStateMessageCallback(StateCacheList<StateCache> stateList)
    {
        if (stateList != null)
        {
            // Re-populate the SelectedCategory prorperty
            SelectedCategory = (from w in stateList
                                where w.MemberName == "SelectedCategory"
                                select w.MemberValue).FirstOrDefault() as Category;
        }
    }
    
    private void SaveStateForMe()
    {
        // Send message for saving state            
        var stateList = new StateCacheList<StateCache>{
                    new StateCache { ModuleID = ModuleID.ProductListViewModel, 
                                     MemberName = "SelectedCategory",
                                     MemberValue = SelectedCategory,
                                   } 
                    // Other StateCache item can be added here...
        };
        Messenger.Default.Send(stateList, MessageToken.PutStateMessage);
    }
  9. Now where is the best place for calling the SaveStateForMe method before leaving the current screen? We can call the method by passing the Unloaded event handler for the ProductList View to the ViewModel through the EventTrigger and RelayCommand. However, we have already had the Cleanup routines that are executed before closing the screen. We can call for sending saving state message from the Cleanup method of the ViewModel.

  10. public override void Cleanup()
    {
        // Call for saving state info
        SaveStateForMe();
    
        // - - - Remaining code lines...            
    }

    Note that all processes and communications regarding saving and receiving the state data occur only between different ViewModel modules from different assemblies in a totally decoupled style. When adding new screens requiring the state persistence into the application, we only need to add the code pieces into the ViewModel of the new screens.

  11. Run the application and open the Product List screen. Select a category and add or edit/save some items before switching to any other screen. From the Another Screen or Another Xap screen, go back to the Product List screen, the previously displayed data should still be there.

  12. 38.png

Summary

Based on the illustrations in three parts of the article series, we use those procedures to migrate a Silverlight demo application from the basic patterns to the MVVM and MEF composable patterns. We also add some new features with composable MVVM into the application. The approaches of architecture design and code implementation can easily be extended and apply to real world Silverlight business applications. Hope that the article series is helpful to developers who are interested in the topic.

References

  1. Attributed Programming Model Overview (MSDN document)
  2. Building Composable Apps in .NET 4 with the Managed Extensibility Framework (By Glenn Block, MSDN Magazine)
  3. A Pluggable Architecture for Building Silverlight Applications with MVVM (By Weidong Shen, codeproject.com)
  4. Using the MVVM Pattern in Silverlight Applications (By Microsoft Silverlight Team, silverlight.net)
  5. MVVMLight Messages (by rongchaua.net)

License

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

Share

About the Author

Shenwei Liu

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

 
BugDownload has some problem Pinmemberd221e33124-May-12 22:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141220.1 | Last Updated 3 May 2012
Article Copyright 2012 by Shenwei Liu
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid