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.
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.
namespace ProductApp.Common
{
public interface IOtherModel
{
string DemoTextData { get; set; }
}
}
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.
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;}
}
}
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.
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()
{
_lazyAnotherModel =
ModuleCatalogService.Container.GetExport<IOtherModel>(ModuleID.AnotherScreenModel);
_anotherModel = _lazyAnotherModel.Value;
DemoText = _anotherModel.DemoTextData;
}
private string _demoText;
public string DemoText
{
get { return _demoText; }
set
{
if (!ReferenceEquals(_demoText, value))
{
_demoText = value;
RaisePropertyChanged("DemoText");
}
}
}
}
}
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.
<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)
{
DataContext = ModuleCatalogService.Instance.GetModule(ModuleID.AnotherScreenViewModel);
}
}
}
}
After doing the list above, the folder and file structure of the ProductApp.Main looks like this.
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.
case ModuleID.AnotherScreenView:
Messenger.Default.Send(ModuleID.AnotherScreenView, MessageToken.LoadScreenMessage);
_currentViewText = ModuleID.AnotherScreenView;
break;
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.
case ModuleID.AnotherScreenView:
newScreen = _catalogService.GetModule(ModuleID.AnotherScreenView);
break;
Add the second HyperlinkButton
under the first one in the MainPage.xaml file.
<Rectangle x:Name="Divider1" Style="{StaticResource DividerStyle}" />
<HyperlinkButton x:Name="linkButton_AnotherScreen"
Style="{StaticResource LinkStyle}" Content="Another Screen"
Command="{Binding Path=LoadModuleCommand}"
CommandParameter="AnotherScreenView" />
Run the application. Click the Another Screen link buttons to open the screen on the browser. Switching between exported modules can now be performed.
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
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.
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.
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 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.
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.
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.
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.
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 of OnLoadModuleCommand
method.
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)
{
Messenger.Default.Send(ModuleID.AnotherXapView, MessageToken.LoadScreenMessage);
_currentViewText = ModuleID.AnotherXapView;
}
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.
case ModuleID.AnotherXapView:
newScreen = _catalogService.GetModule(ModuleID.AnotherXapView);
break;
Add another HyperlinkButton
under the existing ones in the MainPage.xaml file.
<Rectangle x:Name="Divider2" Style="{StaticResource DividerStyle}" />
<HyperlinkButton x:Name="linkButton_AnotherXap"
Style="{StaticResource LinkStyle}" Content="Another Xap"
Command="{Binding Path=LoadModuleCommand}"
CommandParameter="AnotherXapView" />
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.
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 the OnLoadScreenMessage
method of the MainPage.xaml.cs.
var viewToCleanUp = MainContent.Content as ICleanup;
if (viewToCleanUp != null)
{
viewToCleanUp.Cleanup();
_catalogService.ReleaseModule((IModule)MainContent.Content);
}
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.
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.
public void Cleanup()
{
if (_addProdScreen != null)
{
((ICleanup)_addProdScreen).Cleanup();
ModuleCatalogService.Instance.ReleaseModuleLazy((IModule)_addProdScreen);
_addProdScreen = null;
}
if (DataContext != null)
{
ModuleCatalogService.Instance.ReleaseModule((IModule)DataContext);
DataContext = null;
}
Messenger.Default.Unregister(this);
}
Add the below Cleanup
method into the ProductListViewModel.cs which overrides the same method in the ViewModelBase
class from the MVVMLight.
public override void Cleanup()
{
if (_productListModel != null)
{
_productListModel.GetCategoryLookupComplete -= ProductListModel_GetCategryComplete;
_productListModel.GetCategorizedProductsComplete -= ProductListModel_GetCategorizedProductComplete;
_productListModel.SaveChangesComplete -= ProductListModel_SaveChangesComplete;
_productListModel = null;
}
_categoryItems = null;
_productItems = null;
_selectedCategory = null;
_currentProduct = null;
_comboDefault = null;
base.Cleanup();
}
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.
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)
{
_anotherModel.Cleanup();
ModuleCatalogService.Container.ReleaseExport<IOtherModel>(_lazyAnotherModel);
_anotherModel = null;
_lazyAnotherModel = null;
_demoText = null;
}
base.Cleanup();
}
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.
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.
namespace ProductApp.Common
{
public class StateCache
{
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()
{
}
}
}
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.
private StateCacheList<StateCache> _mainStateList;
public MainPageViewModel()
{
Messenger.Default.Register(this, MessageToken.PutStateMessage,
new Action<StateCacheList<StateCache>>(OnPutStateMessage));
_mainStateList = new StateCacheList<StateCache>();
Messenger.Default.Register<NotificationMessageAction<StateCacheList<StateCache>>>
(this, MessageToken.GetStateMessage, message => OnGetStateMessage(message));
}
private void OnPutStateMessage(StateCacheList<StateCache> stateList)
{
if (stateList != null)
{
if (_mainStateList != null)
{
_mainStateList.RemoveAll(m => m.ModuleID == stateList[0].ModuleID);
}
_mainStateList.AddRange(stateList);
stateList = null;
}
}
private void OnGetStateMessage(NotificationMessageAction<StateCacheList<StateCache>> message)
{
if (message != null)
{
StateCacheList<StateCache> stateList = new StateCacheList<StateCache>();
stateList.AddRange(_mainStateList.Where(w => w.ModuleID == message.Notification));
if (stateList.Count > 0)
{
message.Execute(stateList);
stateList = null;
}
}
}
Open the ProductListViewModel.cs file, add the following code to the constructor for requesting the state data when opening the
Product List screen.
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)
{
SelectedCategory = (from w in stateList
where w.MemberName == "SelectedCategory"
select w.MemberValue).FirstOrDefault() as Category;
}
}
private void SaveStateForMe()
{
var stateList = new StateCacheList<StateCache>{
new StateCache { ModuleID = ModuleID.ProductListViewModel,
MemberName = "SelectedCategory",
MemberValue = SelectedCategory,
}
};
Messenger.Default.Send(stateList, MessageToken.PutStateMessage);
}
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.
public override void Cleanup()
{
SaveStateForMe();
}
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.
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.
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)