Silverlight 应用程序从基础迁移到 MVVM 和 MEF 可组合模式 - 第 2 部分
本文系列展示了如何通过简单的方法和详细的代码解释,将具有基本模式的Silverlight应用程序升级为MVVM和MEF可组合模式。
引言
在系列文章的第 1 部分中,我们已经开始了将之前使用基本导航和代码隐藏模式的 Silverlight 应用程序更改为 MVVM 和 MEF 可组合模式的工作。在第 1 部分结束时,该应用程序能够加载 xap 文件,将类模块导出到组合容器,并在浏览器中渲染与更改前相同的屏幕。在本部分中,我们将根据第 1 部分开头所示的架构设计,为 MainPage 用户控件、产品列表父屏幕和子窗口实现可组合的 MVVM 模块。
内容和链接
- 第一部分 - 开始模式更改工作
- 第二部分 - 转换为可组合MVVM
- 第三部分 - 扩展应用程序
设置 MVVMLight 库
如今,使用 NuGet 是为使用 Visual Studio 开发的应用程序设置库的推荐方法。但是,我不喜欢 Galasoft MVVMLight 的做法,即通过 Visual Studio 从 NuGet 下载时,会添加所有旧版本 .NET Framework 的程序集,总大小为 2.5 MB。相反,我们只需要两个 dll 文件,GalaSoft.MvvmLight.SL5.dll 和 System.Windows.Interactivity.dll,总大小仅为 56 KB。这次我们将手动加载这两个文件,并从需要它们的项目设置引用。
-
使用 Windows Explorer 在解决方案的根文件夹中创建一个名为 _Assemblies 的物理文件夹。此文件夹可用于存放所有共享的程序集源。
-
将 GalaSoft.MvvmLight.SL5.dll 和 System.Windows.Interactivity.dll 这两个文件复制并粘贴到 _Assemblies 文件夹中。您可以在本系列文章此部分下载的源代码包的 _Assemblies 文件夹中找到这些文件。
-
共享程序集文件夹和文件不必显示在 Solution Explorer 中。但如果您愿意,可以在解决方案根目录下创建一个同名的虚拟解决方案文件夹 _Assemblies。将文件从物理 _Assemblies 文件夹复制,然后粘贴到 Solution Explorer 中的虚拟文件夹中。Solution Explorer 中显示的文件是虚拟副本。
-
使用“添加引用”屏幕上的“浏览”选项卡,在 ProductApp.Main、ProductApp.Views 和 ProductApp.Common 项目中创建对这两个 dll 程序集的引用。
MVVMLight 库提供了命令、消息传递和清理功能,减少了我们额外的编码工作。我们将直接调用库中的函数,但在发送消息和显示对话框文本时,我们在 ProductApp.Common 项目的 Constants 文件夹中添加了两个新的类文件:MessageToken.cs 和 StaticText.cs。您也可以在下载的源代码包中找到这些文件。
为 MainPage 使用 ViewModel
按设计,MainPage.xaml 及其代码隐藏是主要的内容持有者(或切换面板),因此它没有用于数据处理的模型。我们需要将代码隐藏中的进程移至 MainPageViewModel 类,但排除与 UI 和加载视图模块相关的进程。
-
将 System.ComponentModel.Composition(.NET)引用添加到 ProductApp.Main 项目中。
-
在 ProductApp.Main 项目中添加一个名为 ViewModels 的新文件夹,并在该文件夹中添加一个名为 MainPageViewModel.cs 的新类文件。
-
将 MainPageViewModel.cs 中的自动生成代码替换为以下代码。
MainPageViewModel
类继承自 MVVMLight 中的ViewModelBase
类,该类具有RaisePropertyChanged
和Cleanup
函数。现在,MainPageViewModel
类负责加载 xap 文件,然后将消息发送回视图代码隐藏,以便从 xap 导入模块。using System; using System.Linq; using System.Windows.Controls; using System.ComponentModel; using System.ComponentModel.Composition; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; using GalaSoft.MvvmLight.Messaging; using ProductApp.Common; namespace ProductApp.Main.ViewModels { [Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, ModuleID.MainPageViewModel)] [PartCreationPolicy(CreationPolicy.NonShared)] public class MainPageViewModel : ViewModelBase, IModule { private ModuleCatalogService _catalogService = ModuleCatalogService.Instance; private string _currentViewText = string.Empty; public MainPageViewModel() { } // Defined with string type to pass the command parameter private RelayCommand<string> _loadModuleCommand; // public RelayCommand<string> LoadModuleCommand { get { if (_loadModuleCommand == null) { // Parameter 1: define a delegate method to be executed // Parameter 2: the delegate function CanExedute // with one-in(object) and one-out(bool) parameters // to make the command enabled or disabled _loadModuleCommand = new RelayCommand<string>( OnLoadModuleCommand, moduleId => moduleId != null); } return _loadModuleCommand; } } // private void OnLoadModuleCommand(String moduleId) { string xapUri; try { if (_currentViewText != moduleId) { // For loading Xap or View switch (moduleId) { // Load ProductApp.Xap on-demand case ModuleID.ProductListView: xapUri = "/ClientBin/ProductApp.Views.xap"; _catalogService.AddXap(xapUri, arg => ProductApp_OnXapDownloadCompleted(arg)); break; // Add for other xaps or modules here default: throw new NotImplementedException(); } } } catch (Exception ex) { // Pass error object to View code-behind Messenger.Default.Send(ex, MessageToken.RaiseErrorMessage); } } private void ProductApp_OnXapDownloadCompleted(AsyncCompletedEventArgs e) { // Send message back to View code-behind to load ProductList View Messenger.Default.Send(ModuleID.ProductListView, MessageToken.LoadScreenMessage); // Cache for check repeat commands later _currentViewText = ModuleID.ProductListView; } } }
-
将 MainPage.xaml 代码中的
HyperlinkButton
的属性更改为具有发送到 MainPageViewModel 的Command
和CommandParameter
。Command
的路径名应与具有RelayCommand
类型的属性名相同。CommandParameter
的值也应与ModuleID
相同。<HyperlinkButton x:Name="linkButton_ProductList" Style="{StaticResource LinkStyle}" Content="Product List" Command="{Binding Path=LoadModuleCommand}" CommandParameter="ProductListView" />
-
将 MainPage.xaml.cs 中的现有代码替换为以下代码。代码隐藏注册消息处理程序,加载导出的视图模块,动态更改 UI 属性,并在必要时通过对话框显示错误或信息消息。
using System; using System.Windows; using System.Windows.Controls; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Messaging; using ProductApp.Common; namespace ProductApp.Main.Views { public partial class MainPage : UserControl { private ModuleCatalogService _catalogService = ModuleCatalogService.Instance; public MainPage() { InitializeComponent(); // Register MVVMLight message handers Messenger.Default.Register(this, MessageToken.LoadScreenMessage, new Action<string>(OnLoadScreenMessage)); Messenger.Default.Register(this, MessageToken.RaiseErrorMessage, new Action<Exception>(OnRaiseErrorMessage)); Messenger.Default.Register(this, MessageToken.UseDialogMessage, new Action<DialogMessage>(OnUseDialogMessage)); if (!ViewModelBase.IsInDesignModeStatic) { // Import the ViewModel module into the View DataContext so that // the members of the ViewModel can be exposed DataContext = _catalogService.GetModule(ModuleID.MainPageViewModel); } } // Method to be executed when receiving the message private void OnLoadScreenMessage(string moduleId) { object newScreen; try { // Import selected View module and then // set the commandArg for UI changes switch (moduleId) { case ModuleID.ProductListView: newScreen = _catalogService.GetModule(ModuleID.ProductListView); break; // Add for other modules here default: throw new NotImplementedException(); } // Set the new screen MainContent.Content = newScreen; // UI - Set link button state SetLinkButtonState(moduleId); } catch (Exception ex) { OnRaiseErrorMessage(ex); } } // UI private void SetLinkButtonState(string buttonArg) { foreach (UIElement child in LinksStackPanel.Children) { HyperlinkButton hb = child as HyperlinkButton; if (hb != null && hb.Command != null) { if (hb.CommandParameter.ToString().Equals(buttonArg)) { VisualStateManager.GoToState(hb, "ActiveLink", true); } else { VisualStateManager.GoToState(hb, "InactiveLink", true); } } } } private void OnRaiseErrorMessage(Exception ex) { // Error message display ChildWindow errorWin = new ErrorWindow(ex.Message, ex.StackTrace); errorWin.Show(); } private void OnUseDialogMessage(DialogMessage dialogMessage) { // MVVMLight DialogMessage callback processes if (dialogMessage != null) { MessageBoxResult result = MessageBox.Show(dialogMessage.Content, dialogMessage.Caption, dialogMessage.Button); dialogMessage.ProcessCallback(result); } } } }
-
现在,使用
MainPageViewModel
类的新命令工作流,应用程序应该可以正常运行。
使用可组合 MVVM 更新 ProductList
将进程从 ProductList 代码隐藏移至 ProductListViewModel 的任务与 MainPage
用户控件的任务基本相同。主要区别在于,ProductListViewModel 中的一些方法和属性与其中模型类的数据操作以及视图中的数据绑定相关。本节将更关注这些差异。
-
在解决方案中添加一个名为 ProductApp.Client 的虚拟文件夹,并将现有的 ProductApp.Views 拖放到该虚拟文件夹中。
-
在 ProductApp.Client 虚拟文件夹下添加两个新的 Silverlight Class Library 项目,名称分别为 ProductApp.ViewModels 和 ProductApp.Models。最快简单的方法是创建 ProductApp.Common 的自定义模板,使用它来创建新的类库项目,然后删除新项目中不需要的附加项,就像我们在本系列文章的前一部分中所做的那样。新项目已设置了所有必需的引用,除了 ProductApp.Common。
-
将 ProductApp.Common 项目的引用添加到两个新项目中。
-
将这两个新项目的引用添加到 ProductApp.Views 项目中。用于 ViewModel 和 Model 的两个新项目不是单独加载的程序集,需要链接到父项目以导出它们的模块。
-
添加一些文件夹和文件,这些文件主要在 ProductApp.Models 项目中执行数据操作,如下所示。
要从视图访问 ViewModel 成员,我们可以将视图的内置
DataContext
设置为持有已导出 ViewModel 的实例。但是,要从 ViewModel 访问 Model 成员,我们需要创建自己的接口类型。这就是 IProductListModel.cs 的作用。它遵循 loC(控制反转)模式标准,尽管它没有提供完全解耦的场景。using System; using System.ComponentModel; using GalaSoft.MvvmLight; using ProductRiaLib.Web.Models; using ProductRiaLib.Web.Services; namespace ProductApp.Common { public interface IProductListModel : INotifyPropertyChanged, ICleanup { // Exposed data operation method and event handler pairs void GetCategoryLookup(); event EventHandler<QueryResultsArgs<Category>> GetCategoryLookupComplete; void GetCategorizedProducts(int categoryId); event EventHandler<QueryResultsArgs<Product>> GetCategorizedProductsComplete; void SaveChanges(string operationType); event EventHandler<SubmitOperationArgs> SaveChangesComplete; // Other exposed methods and properties void AddNewProduct(Product addedProduct); void DeleteProduct(Product deletingProduct); string CurrentOperation { get; set; } Boolean HasChanges { get; } Boolean IsBusy { get; } } }
DataAsyncHandlers.cs 包含两个包装函数,用于调用 RIA Domain Services 的
ProductDomainContext
中的Load
和SubmitChanges
方法。我们需要调用自定义包装函数并提供适当的自定义参数,以等待加载查询后的数据集返回,或者在 MVVM 和 MEF 可组合模式的异步操作中提交数据后的状态。在基本模式的应用程序中,当在 SilverLight User Control 代码隐藏中创建域上下文实例或使用DomainDataSource
控件时,异步问题会自动处理。例如,在我们旧的 ProductList.xaml.cs 代码隐藏中,我们只需调用Load
函数并从中获取数据ctx
,如下所示。ctx.Load(ctx.GetCategoriesQuery());
更新到新模式后,我们需要通过传递四个参数来调用 Load 函数,并从回调参数
e
中获取数据集。context.Load(ctx.GetCategoriesQuery(), LoadBehavior.RefreshCurrent, r => { queryResultEvent(s, e); }, null);
详细的数据操作不是本系列文章的重点,因此我们不在此展示 DataAsyncHandlers.cs、OperationTypes.cs 以及 EventArguments 文件夹中文件的代码细节。您可以直接从下载的源代码包中复制和使用这些文件。
-
向 ProductApp.Models 项目添加一个名为 ProductListModel.cs 的类文件。该类被导出为共享模块,并定义了三个用于数据操作的事件处理程序。
[Export(ModuleID.ProductListModel, typeof(IProductListModel))] [PartCreationPolicy(CreationPolicy.Shared)] public class ProductListModel : IProductListModel { private ProductDomainContext _ctx; // Define event handlers for data operations public event EventHandler<QueryResultsArgs<Category>> GetCategoryLookupComplete; public event EventHandler<QueryResultsArgs<Product>> GetCategorizedProductsComplete; public event EventHandler<SubmitOperationArgs> SaveChangesComplete; // Remaining code - - - }
类中的其余代码很简单,主要是对
IProductListModel
接口中定义的成员的实现。您可以从下载的源代码包中复制代码行或文件,然后检查详细信息。 -
向 ProductApp.ViewModels 项目添加一个名为 ProductListViewModel.cs 的类文件。该类的内容实现与我们之前完成的 MainPageViewModel.cs 基本相同,只是增加了更多与 CRUD 数据操作命令和进程相关的成员。此处未显示完整的代码片段。您可以从下载的源代码包中的 ProductListViewModel.cs 文件复制代码,然后在此处检查详细信息。以下是一些关于此类中特定成员和代码行的补充说明。
该类通过直接调用组合容器的方法导入 ProductListModel 模块,并将其作为
IProductListModel
类型的Lazy
对象公开,这种方式与将 ViewModel 导出到容器的方式略有不同。该类还接收从 Model 引发的事件,以便在异步数据操作后继续执行某些操作。此代码片段说明了这一点。public ProductListViewModel() // Constructor { // Inject the Model as the Lazy object with the defined type _productListModel = ModuleCatalogService.Container.GetExport<IProductListModel>(ModuleID.ProductListModel).Value; // Register the event handlers defined in the Model _productListModel.GetCategoryLookupComplete += ProductListModel_GetCategryComplete; // - - - } private void ProductListModel_GetCategryComplete(object sender, QueryResultsArgs<Category> e) { if (!e.HasError) { // Set the returned data from async loading to data object CategoryItems = e.Results; if (SelectedCategory == null) { // Add the combo default item "Please Select" CategoryItems.Insert(0, _comboDefault); // Set selected category if not from state cache SelectedCategory = CategoryItems[0]; } } else { // Send error message Messenger.Default.Send(e.Error, MessageToken.RaiseErrorMessage); } }
ProductListViewModel
类中的代码还接收来自视图的两个特殊命令,用于非按钮元素,然后执行所需的动作。命令触发问题将在 列表 9 中讨论。public RelayCommand CategorySelectionChanged { // - - - } // private void OnCategorySelectionChanged() { // Reload the data to context based on the selected category Category item = SelectedCategory; if (item != null && item.CategoryID > 0) { _productListModel.GetCategorizedProducts((int)item.CategoryID); } // Remove the "Please Select" if (SelectedCategory != _comboDefault) { CategoryItems.Remove(_comboDefault); } // Enable the Add button AddProductCommand.RaiseCanExecuteChanged(); } public RelayCommand DataGridSelectionChanged { // - - - } // private void OnDataGridSelectionChanged() { // Enable buttons SaveChangesCommand.RaiseCanExecuteChanged(); DeleteProductCommand.RaiseCanExecuteChanged(); }
-
现在更新 ProductList.xaml.cs 代码隐藏中的代码。该类中只包含非常简单的代码行。
using System; using System.Windows; using System.Windows.Controls; using System.ComponentModel.Composition; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Messaging; using ProductApp.Common; namespace ProductApp.Views { [Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, ModuleID.ProductListView)] public partial class ProductList : UserControl, IModule { private ModuleCatalogService _catalogService = ModuleCatalogService.Instance; public ProductList() { InitializeComponent(); if (!ViewModelBase.IsInDesignModeStatic) { // Set the DataContext to the imported ViewModel DataContext = ModuleCatalogService.Instance.GetModule(ModuleID.ProductListViewModel); } } } }
-
通过从下载的源代码包复制/粘贴代码行或整个文件来更新现有的 ProductList.xaml 文件。请注意,
System.Windows.Interactivity
引用已添加到xmlns
声明部分。xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
ComboBox
、DataGrid
和Buttons
的代码行已更改。Command
绑定路径指向RelayCommand
类型的属性。使用System.Windows.Interactivity
程序集定义了一个额外的EventTrgger
节点,其中包含附加到ComboBox
的内置事件SelectionChanged
。xaml 代码实际上将System.Windows.Interactivity.InvokeCommandAction
类的Command
属性设置为ProductListViewModel
类中RelayCommond
类型的属性。对于DataGrid
也实现了相同的操作。<ComboBox Height="23" Margin="6" Name="categoryCombo" Width="150" ItemsSource="{Binding Path=CategoryItems}" DisplayMemberPath="CategoryName" SelectedValuePath="CategoryID" SelectedItem="{Binding Path=SelectedCategory, Mode=TwoWay}"> <i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <i:InvokeCommandAction Command="{Binding CategorySelectionChanged}" /> </i:EventTrigger> </i:Interaction.Triggers> </ComboBox>
修改子窗口
子窗口 AddProductWindow.xaml 与父窗口共享 ProductListModel 模块用于数据操作。子窗口也是从父视图打开的,而不是从 MainPage 视图打开。父屏幕和子窗口之间的所有通信都通过完全解耦的事件消息传递方法在父 ViewModel 和子 ViewModel 之间进行。
-
在 ProductApp.ViewModels 项目中添加名为 AddProductWindowViewModel.cs 的新类文件。更新代码或用下载的源代码包中的代码替换文件。所有导出设置和命令属性与我们在其他模块中所做的类似。该类注册一个消息处理程序以接收数据绑定所需的
SelectedCategory
对象。public AddProductWindowViewModel() // Constructor { Messenger.Default.Register(this, MessageToken.DataToAddProductVmMessage, new Action<Category>(OnDataToAddProductVmMessage)); } private void OnDataToAddProductVmMessage(Category selectedCategory) { SelectedCategory = selectedCategory; AddedProduct = new Product(); }
在收到将产品添加到数据库的命令时,该类发送第一个消息,其中包含
AddedProduct
对象,以便父 ViewModel 更新数据。然后,它发送另一条消息,其中包含状态信息,以便父视图显示。private void OnOKButtonCommand() { if (AddedProduct != null) { AddedProduct.CategoryID = SelectedCategory.CategoryID; Messenger.Default.Send(AddedProduct, MessageToken.DataToProductListVmMessage); Messenger.Default.Send("OK", MessageToken.AddProductWindowMessage); } }
在父端,ProductListViewModel 模块注册消息处理程序以接收
AddedProduct
对象,并使用事件例程调用数据操作和刷新显示。public ProductListViewModel() // Constructor { Messenger.Default.Register(this, MessageToken.DataToProductListVmMessage, new Action<Product>(OnDataToProductListVmMessage)); } private void OnDataToProductListVmMessage(Product addedProduct) { if (!_productListModel.IsBusy && addedProduct != null) { // Add to Context in the model _productListModel.AddNewProduct(addedProduct); // Add to ObCollection for refreshing display ProductItems.Add(addedProduct); } }
-
更新 AddProductWindow.xaml.cs 文件,方法是复制下载的源代码包中的代码或文件。除了将
DataContext
设置为导出的 AddProductWindowViewModel 的实例外,该类还接收来自 ViewModel 的消息,以响应 OK 或 Cancel 按钮的点击操作。public AddProductWindow() // Constructor { InitializeComponent(); Messenger.Default.Register(this, MessageToken.AddProductWindowMessage, new Action<string>(OnAddProductWindowMessage)); if (!ViewModelBase.IsInDesignModeStatic) { // Set the DataContext to the imported ViewModel DataContext = ModuleCatalogService.Instance.GetModule(ModuleID.AddProductWindowViewModel); } } private void OnAddProductWindowMessage(string buttonName) { switch (buttonName) { case "OK": DialogResult = true; break; case "Cancel": DialogResult = false; break; default: break; } }
-
AddProductWindow.xaml 文件中的更改是命令绑定设置和文本框的数据绑定。代码行与父 xaml 文件中的代码行相似。代码在此处列出。您可以复制代码或从下载的源代码包中复制文件来更新 AddProductWindow.xaml 文件。
-
在父视图 ProductList.xaml.cs 代码隐藏中添加代码行以加载和打开子窗口。
private ChildWindow _addProdScreen; public ProductList() // Constructor { // Register the massage handler for loading child window Messenger.Default.Register(this, MessageToken.LoadAddProductViewMessage, new ActionA<string>(OnLoadAddProductViewMessage)); // - - - Other code lines } private void OnLoadAddProductViewMessage(string message) { // Load AddProductWindowView lazy module _addProdScreen = _catalogService.GetModuleLazy(ModuleID.AddProductWindowView) as ChildWindow; _addProdScreen.Show(); }
-
运行应用程序。Product List 屏幕和 Add Product Window 现在可以与 MVVM 和 MEF 可组合模式配合使用,尽管屏幕和内容显示与之前使用基本模式时相同。
摘要
在本系列文章的这一部分中,我们设置了应用程序使用的 MVVMLight 库。然后,我们将 MainPage 控件以及 Product List 屏幕及其子窗口从基本模式升级为 MVVM 和 MEF 可组合模式。在 第 3 部分中,我们将向 ProductApp.Main 项目添加另一个屏幕,为新的 xap 程序集在解决方案中创建另一组项目,实现模块清理进程,并向应用程序添加状态持久化功能。