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





5.00/5 (4 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
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
- Part 1 - Start the Work on Pattern Changes
- Part 2 - Convert to the Composable MVVM
- Part 3 - Extend the Application
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.
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.
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.
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 itsValue
separately. TheValue
property of theLazy
object will be set to the class/module instance whereas theLazy
object, not itsValue
, will be cleaned up when leaving the screen.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 theGrid
node of the xaml file.Open the MainPageViewModels.cs and add the following code piece into the
switch
block ofOnLoadModuleCommand
method. After receiving the navigation command, the ViewModel sends a message to the View for loading the requested screen.Open the MainPage.xaml.cs and add the following code piece into the
switch
block ofOnLoadScreenMessage
method for loading the AnotherScreen.xaml View to the content holder.Add the second
HyperlinkButton
under the first one in the MainPage.xaml file.Run the application. Click the Another Screen link buttons to open the screen on the browser. Switching between exported modules can now be performed.
namespace ProductApp.Common
{
public interface IOtherModel
{
string DemoTextData { get; set; }
}
}
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;}
}
}
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");
}
}
}
}
}
<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.
// Send message back to MainPageView code-behind to load AnotherScreenView
case ModuleID.AnotherScreenView:
Messenger.Default.Send(ModuleID.AnotherScreenView, MessageToken.LoadScreenMessage);
_currentViewText = ModuleID.AnotherScreenView;
break;
case ModuleID.AnotherScreenView:
newScreen = _catalogService.GetModule(ModuleID.AnotherScreenView);
break;
<Rectangle x:Name="Divider1" Style="{StaticResource DividerStyle}" />
<HyperlinkButton x:Name="linkButton_AnotherScreen"
Style="{StaticResource LinkStyle}" Content="Another Screen"
Command="{Binding Path=LoadModuleCommand}"
CommandParameter="AnotherScreenView" />
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.
Create a virtual folder, AnotherXap.Client, in the solution.
Before creating the custom templates in the next step, let's set
Copy Local
toFalse
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.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.
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 theAnotherXap.Models
in the code. - Replace all instances of
AnotherScreen
withAnotherXap
in the current document. - Replace the value of
DemoTextData
property with the"IT'S THE MODULE FORM ANOTHER ZAP"
or something else. 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 theAnotherXap.ViewModels
in the code. - Replace all instances of
AnotherScreen
withAnotherXap
in the current document. 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 theAnotherXap.Views
in the code of both .xaml and .cs files. - Replace all instances of
AnotherScreen
withAnotherXap
in the code of both .xaml and .cs files. Open the App.xaml.cs and replace the code
ProductList
that was carried over from the template with the codeAnotherXap
in theApplication_Startup
method.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.
Open the MainPageViewModels.cs and add the following code piece into the
switch
block ofOnLoadModuleCommand
method.Open the MainPage.xaml.cs and add the following code piece into the
switch
block ofOnLoadScreenMessage
method for loading the AnotherXap View to the content holder.Add another
HyperlinkButton
under the existing ones in the MainPage.xaml file.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.
Run the application to test the three link buttons and the screen contents.
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.
Do the same for the ProductApp.ViewModel and ProductApp.Model but select all reference items there.
The project and files under the AnotherXap.Client virtual folder are shown below.
// 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;
}
case ModuleID.AnotherXapView:
newScreen = _catalogService.GetModule(ModuleID.AnotherXapView);
break;
<Rectangle x:Name="Divider2" Style="{StaticResource DividerStyle}" />
<HyperlinkButton x:Name="linkButton_AnotherXap"
Style="{StaticResource LinkStyle}" Content="Another Xap"
Command="{Binding Path=LoadModuleCommand}"
CommandParameter="AnotherXapView" />
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.
Add the code piece below into the place right after the
switch
block in theOnLoadScreenMessage
method of the MainPage.xaml.cs.Go to the ProductApp.Views project, add the
ICleanup
into the inheritance list for theProductList
class in the ProductList.xaml.cs. Note that all View code-behind modules should implement theICleanup
interface from the MVVMLight, except the MainPage.xaml.cs.Add the
Cleanup
method shown below into the ProductList.xaml.cs code-behind. The code firstly calls theCleanup
method in its child window module and then calls catalog service to release the ProductListViewModel module. TheReleaseModule
method in theModuleCatalogService
class automatically calls theCleanup
method in the ViewModel module before it releases the ViewModel. The similar clean-up workflow occurs for the child window modules.Add the below
Cleanup
method into the ProductListViewModel.cs which overrides the same method in theViewModelBase
class from the MVVMLight.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.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.
Set breakpoints inside the
Cleanup
method in any View or ViewModel, or inside theModuleCatalogService.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.
// 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);
}
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);
}
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();
}
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();
}
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.
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.
The code lines in the StateCache.cs file should be like this.
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.
Open the ProductListViewModel.cs file, add the following code to the constructor for requesting the state data when opening the Product List screen.
Now where is the best place for calling the
SaveStateForMe
method before leaving the current screen? We can call the method by passing theUnloaded
event handler for the ProductList View to the ViewModel through theEventTrigger
andRelayCommand
. However, we have already had theCleanup
routines that are executed before closing the screen. We can call for sending saving state message from theCleanup
method of the ViewModel.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.
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()
{
}
}
}
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;
}
}
}
// 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);
}
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.
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
- Attributed Programming Model Overview (MSDN document)
- Building Composable Apps in .NET 4 with the Managed Extensibility Framework (By Glenn Block, MSDN Magazine)
- A Pluggable Architecture for Building Silverlight Applications with MVVM (By Weidong Shen, codeproject.com)
- Using the MVVM Pattern in Silverlight Applications (By Microsoft Silverlight Team, silverlight.net)
- MVVMLight Messages (by rongchaua.net)