Silverlight 应用程序从基础迁移到 MVVM 和 MEF 可组合模式 - 第 3 部分
本文系列展示了如何通过简单的方法和详细的代码解释,将具有基本模式的Silverlight应用程序升级为MVVM和MEF可组合模式。
引言
完成本文系列前几部分所述的工作后,我们已经创建了主内容持有者项目,并将“产品列表”屏幕及其子窗口从基本模式升级为MVVM和MEF可组合模式。在本文中,我们将向“ProductApp.Main”项目添加另一个演示屏幕,并在解决方案中创建另一组项目以用于新的xap程序集,以便我们可以在导出的模块和xap程序集之间切换屏幕。然后,我们将实现模块清理过程,并向应用程序添加状态持久化功能。
目录和链接
- 第一部分 - 开始模式更改工作
- 第二部分 - 转换为可组合MVVM
- 第三部分 - 扩展应用程序
向主客户端项目添加另一个屏幕
此屏幕提供了一个在同一项目以及未导出的xap程序集中实现可组合MVVM的示例。为方便演示,我们将保持此屏幕的模块尽可能简单。任何必需的模块ID字符串常量已在“ProductApp.Common\Constants\ModuleID.cs”文件中。
在“ProductApp.Common”项目中,向“Model”文件夹添加一个接口文件“IOtherModel”。它将用作新ViewModel和Model之间通信的契约。为了演示文本,仅定义了一个字符串属性。
在“ProductApp.Main”项目中,创建一个新文件夹“Models”,然后向该文件夹添加一个新类文件“AnotherScreenModel.cs”。为类添加简单的代码。请注意,这次我们将导出的模块定义为非共享模块的示例。
在同一项目中,将“ViewModels”文件夹中的“AnotherScreenViewModel.cs”文件添加到“ViewModels”文件夹中,并将以下代码放入类中。请注意,我们分别定义了
Lazy
对象及其Value
的变量。Lazy
对象的Value
属性将设置为类/模块实例,而当离开屏幕时,将清理Lazy
对象本身,而不是其Value
。向“ProductApp.Main”项目的“Views”文件夹添加一个名为“AnotherScreen.xaml”的新的Silverlight用户控件,然后在xaml文件的
Grid
节点下添加一个TextBlock
。打开“MainPageViewModels.cs”,并将以下代码块添加到
OnLoadModuleCommand
方法的switch
语句中。在接收到导航命令后,ViewModel会向View发送一条消息以加载请求的屏幕。打开“MainPage.xaml.cs”,并将以下代码块添加到
OnLoadScreenMessage
方法的switch
语句中,以便将“AnotherScreen.xaml”View加载到内容持有者中。在“MainPage.xaml”文件中,在第一个
HyperlinkButton
下方添加第二个HyperlinkButton
。运行应用程序。点击“Another Screen”链接按钮在浏览器中打开屏幕。现在可以执行导出模块之间的切换。
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" />
在“AnotherScreen.xaml.cs”中,用以下代码替换现有代码。
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);
}
}
}
}
完成上述列表后,“ProductApp.Main”的文件夹和文件结构如下所示。
// 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" />
向应用程序添加另一个Xap
完成了前面的工作,向解决方案添加项目以创建新的xap程序集并不困难。我们将使用快捷方式创建xap,并使MVVM模块类似于“Another Screen”链接的模块。
在解决方案中创建一个虚拟文件夹“AnotherXap.Client”。
在下一步创建自定义模板之前,让我们将现有“ProductApp.Client”虚拟文件夹下的“ProductApp.Views”、“ProductApp.ViewModels”和“ProductApp.Models”项目中几乎所有引用的“Copy Local”设置为“False”。“ProductApp.Main”项目会将所有引用的dll文件加载到其bin文件夹中,这些文件可供应用程序中的所有其他客户端项目使用,因此其他程序集不需要在其自己的本地bin文件夹中包含相同的副本。这将减小xap及其引用的ViewModel/Model程序集的大小,从而加快加载速度。使用自定义模板的好处是,现有项目中的所有引用设置将自动应用到新项目中。
从现有“ProductApp.Client”虚拟文件夹下的这三个项目中导出自定义模板,然后使用相应的自定义模板在“AnotherXap.Client”虚拟文件夹下创建三个新项目。新项目的名称分别为“AnotherXap.Views”、“AnotherXap.ViewModels”和“AnotherXap.Models”。
在“AnotherXap.Models”项目中执行更改。
- 删除项目根文件夹中的类文件。
- 将“ProductApp.Main”项目中的“Models\AnotherScreenModel.cs”复制/粘贴到当前项目中,并将其重命名为“AnotherXapModel.cs”。
- 将代码中的命名空间“ProductApp.Main.Models”重命名为“AnotherXap.Models”。
- 将当前文档中的所有“AnotherScreen”实例替换为“AnotherXap”。
- 将“DemoTextData”属性的值替换为“IT'S THE MODULE FORM ANOTHER ZAP”或其他内容。
在“AnotherXap.ViewModels”项目中执行类似的更改。
- 删除项目根文件夹中的类文件。
- 将“ProductApp.Main”项目中的“ViewModels\AnotherScreenViewModel.cs”复制/粘贴到当前项目中,并将其重命名为“AnotherXapViewModel.cs”。
- 将代码中的命名空间“ProductApp.Main.ViewModels”重命名为“AnotherXap.ViewModels”。
- 将当前文档中的所有“AnotherScreen”实例替换为“AnotherXap”。
在“AnotherXap.Views”项目中执行更改。
- 删除项目中根文件夹中的除“App.xaml”及其“.cs”之外的所有xaml和类文件。
- 将“ProductApp.Main”项目中的“Views\AnotherScreen.xaml”文件复制/粘贴到当前项目中,并将其重命名为“AnotherXap.xaml”。使用Visual Studio的“Solution Explorer”时,代码隐藏的“.cs”文件会自动复制和重命名。
- 在两个“.xaml”和“.cs”文件的代码中,将命名空间“ProductApp.Main.Views”重命名为“AnotherXap.Views”。
- 在两个“.xaml”和“.cs”文件的代码中,将所有“AnotherScreen”实例替换为“AnotherXap”。
打开“App.xaml.cs”,并在
Application_Startup
方法中将从模板继承的“ProductList”代码替换为“AnotherXap”代码。在“AnotherXap.Views”项目中,删除从模板继承的“ProductApp.ViewModels”和“ProductApp.Models”的旧引用。然后向当前项目添加新项目“AnotherXap.ViewModels”和“AnotherXap.Models”的引用。
打开“MainPageViewModels.cs”,并将以下代码块添加到
OnLoadModuleCommand
方法的switch
语句中。打开“MainPage.xaml.cs”,并将以下代码块添加到
OnLoadScreenMessage
方法的switch
语句中,以便将“AnotherXap”View加载到内容持有者中。在“MainPage.xaml”文件中,在现有
HyperlinkButton
下方再添加一个HyperlinkButton
。在Web托管服务器“ProductApp.Web”项目中,在项目“Properties”页的“Silverlight Application”选项卡上,使用我们之前添加“ProductApp.Main”的方法,从现有下拉列表中将“AnotherXap.Views”项目添加到Web托管服务器项目中。但这次不需要生成起始测试页面。
运行应用程序以测试三个链接按钮和屏幕内容。
在“ProductApp.Views”项目中,选择除“ProductApp.Models”和“ProductApp.ViewModels”之外的所有引用,右键单击打开“Properties”面板,然后在“Copy Local”项目的下拉列表中选择“False”。
对“ProductApp.ViewModel”和“ProductApp.Model”也执行相同的操作,但选择其中的所有引用项。
“AnotherXap.Client”虚拟文件夹下的项目和文件如下图所示。
// Load AnotherXap on-demand
case ModuleID.AnotherXapView:
xapUri = "/ClientBin/AnotherXap.Views.xap";
_catalogService.AddXap(xapUri, arg => AnotherXap_OnXapDownloadCompleted(arg));
break;
我们需要在类中添加另一个事件例程,以便在xap加载完成后,将消息发送回“MainPage.xml.cs”代码隐藏,请求导出“AnotherXap”View。
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" />
清理非共享模块
MEF可组合应用程序中一个值得注意的问题是组合容器和导出模块的处置。如果处理不当,会导致内存泄漏。对于像本演示应用程序这样的单容器应用程序,容器的处置不是问题。容器的生命周期是用户会话。共享模块也会一直保留到用户关闭应用程序。我们专注于非共享模块的清理任务。以下是基本规则和工作流程。
- 不再需要的模块的清理从请求的新View模块准备加载的那一刻开始。
- 清理顺序是Model、ViewModel和View。
- 任何模块都将其自身成员设置为null,然后通过下一级模块的调用来清理该模块。
- 拥有任何子模块或嵌入式模块的模块负责首先清理其下属模块。
在本演示应用程序中,我们将为所有模块放置清理代码,除了“MainPage”(内容持有者)的View和ViewModel以及“ProductListModel”(共享模块)。由于相似性,我仅展示如何为与“Product List”屏幕相关的模块以及需要特殊解释的其他模块编写清理过程。下载的源代码包包含所有模块的完成代码。
将以下代码块添加到“MainPage.xaml.cs”中
OnLoadScreenMessage
方法switch
语句块的紧后面。转到“ProductApp.Views”项目,在“ProductList.xaml.cs”中为“ProductList”类添加
ICleanup
到继承列表中。请注意,除“MainPage.xaml.cs”外,所有View代码隐藏模块都应实现MVVMLight中的ICleanup
接口。将下面显示的
Cleanup
方法添加到“ProductList.xaml.cs”代码隐藏中。该代码首先调用其子窗口模块中的Cleanup
方法,然后调用目录服务释放“ProductListViewModel”模块。ModuleCatalogService
类中的ReleaseModule
方法会在释放ViewModel之前自动调用ViewModel模块中的Cleanup
方法。对于子窗口模块,也会发生类似的清理工作流程。将下面的
Cleanup
方法添加到“ProductListViewModel.cs”中,它将覆盖MVVMLight中ViewModelBase
类中的同名方法。从下载的源代码包中复制“AddProductWindow.xaml.cs”和“AddProductWindowViewModel.cs”的
Cleanup
方法,并将代码添加到类中。这将完成“Product List”工作流程的清理代码。对剩余部分的清理几乎相同。对于“Another Screen”和“Another Xap”屏幕的模型模块的清理,可能需要提及一个特别的说明。
在任何View或ViewModel的
Cleanup
方法,或ModuleCatalogService.ReleaseModule
方法中设置断点。在调试模式下运行应用程序,然后切换屏幕。您可以使用Visual Studio中的“Autos”、“Locals”或“Quick Watch”窗口查看对象和值的清理过程和结果。
// 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();
}
打开“ProductApp.Common”项目中的“Models\IOtherModel.cs”并添加ICleanup
作为继承接口。
public interface IOtherModel : ICleanup
正如我们之前测试的那样,继承IOtherModel
的模型类是非共享模块。我们需要测试这些模块的清理。 “AnotherScreenViewModels.cs”中的Cleanup
方法负责处理其Model实例_anotherModel
。由于Model被导出为Lazy
对象,因此实际的Model上下文是Lazy
对象的Value
属性。调用Container.ReleaseExport
时,需要将原始Lazy
对象实例作为参数传递。
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();
}
在可组合MVVM屏幕之间持久化状态
如果应用程序未维护状态,用户经常会问“当我回到之前的屏幕时,我的东西去哪儿了?”。状态数据可以存储在数据库中,并在重新加载到之前的屏幕时检索。但是,将状态数据保留在应用程序的本地缓存中,便于在用户会话期间持久化状态,这通常足以满足用户需求。对象缓存通常可以放置在用户身份验证上下文中,但为了演示目的,我们将把状态数据存储在用户会话期间持续存在的“MainPageViewModel”上下文中。我们还将使用消息传递方法,在MVVM环境中以完全解耦的方式传输数据。
在“ProductApp.Common”项目中添加一个名为“SaveState”的新文件夹,然后在该文件夹中添加两个新类文件,“StateCache.cs”和“StateCacheList.cs”。
“StateCache.cs”文件中的代码行应如下所示。
打开“MainPageViewModel.cs”,并将代码行添加到私有变量声明部分下方并重写构造函数。我们使用MVVMLight设置了两个通知消息回调处理程序,用于获取和设置状态数据。
打开“ProductListViewModel.cs”文件,在构造函数中添加以下代码,以便在打开“Product List”屏幕时请求状态数据。
现在,在离开当前屏幕之前调用
SaveStateForMe
方法的最佳位置是什么?我们可以通过EventTrigger
和RelayCommand
将“ProductList”View的Unloaded
事件处理程序传递给ViewModel来调用该方法。但是,我们已经有了在屏幕关闭前执行的Cleanup
例程。我们可以从ViewModel的Cleanup
方法中调用以发送保存状态的消息。运行应用程序并打开“Product List”屏幕。在切换到任何其他屏幕之前,选择一个类别并添加或编辑/保存一些项目。从“Another Screen”或“Another Xap”屏幕返回到“Product List”屏幕,之前显示的数据应该仍然在那里。
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; }
}
}
“StateCacheList.cs”中的代码也很简单。具有强类型StateCache
的StateCacheList
类只有一个空构造函数。
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);
然后,在构造函数下方添加这两个方法,分别用于回调后重新填充数据属性,以及在关闭屏幕前保存状态数据。
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...
}
请注意,所有与保存和接收状态数据相关的过程和通信仅在不同程序集的不同ViewModel模块之间以完全解耦的方式发生。当向应用程序添加需要状态持久化的新屏幕时,我们只需要将代码片段添加到新屏幕的ViewModel中。
摘要
基于本文系列三部分的插图,我们使用这些过程将Silverlight演示应用程序从基本模式迁移到MVVM和MEF可组合模式。我们还使用可组合MVVM向应用程序添加了一些新功能。架构设计和代码实现的方法可以轻松扩展并应用于实际的Silverlight业务应用程序。希望本文系列对对此主题感兴趣的开发者有所帮助。
参考文献
- Attributes Programming Model Overview (MSDN 文档)
- Building Composable Apps in .NET 4 with the Managed Extensibility Framework (Glenn Block, MSDN Magazine)
- A Pluggable Architecture for Building Silverlight Applications with MVVM (Weidong Shen, codeproject.com)
- Using the MVVM Pattern in Silverlight Applications (Microsoft Silverlight Team, silverlight.net)
- MVVMLight Messages (rongchaua.net)