Silverlight 应用程序从基础迁移到 MVVM 和 MEF 可组合模式 - 第 1 部分
本文系列展示了如何通过简单的方法和详细的代码解释,将具有基本模式的Silverlight应用程序升级为MVVM和MEF可组合模式。
引言
我之前的一篇文章展示了一个简单的演示应用程序,该应用程序使用 WCF RIA Services 类库和代码优先的领域数据服务进行 CRUD 数据操作。它有一个主屏幕和一个带有基本导航和代码隐藏模式的子窗口。如果我们将应用程序升级到具有 MVVM 和 MEF 可组合模式的应用程序,会发生什么?这些方法有多容易?编码的细节是什么?本系列文章将通过最简单的方法和详细的编码解释来解决这些问题。完成后的应用程序不会是一个成熟的示例,但应包括 MVVM 和 MEF 可组合模式实现的所有主要方面,而不关注其他一些领域,例如 UI、数据验证、数据服务操作或安全性。我还将描述如何使用新模式处理弹出子窗口,执行可组合部件清理,以及在纯 MVVM 样式中切换屏幕时持久化状态。
目录
- 第一部分 - 开始模式更改工作
- 第二部分 - 转换为可组合MVVM
- 第三部分 - 扩展应用程序
架构简介
现有的演示应用程序结构简单。类直接引用,所有业务逻辑处理代码片段都在代码隐藏分部类中。
当应用程序更新到 MVVM 和 MEF 可组合模式时,可组合部件(在本系列文章中也称为模块)被添加,现有部件也重构为模块,如下图所示。
该图表示以下功能。
- ProductApp.Main.xap 中的视图被设置为主内容持有者。
- xap 程序集可以有其他 MVVM 集(在本例中为另一个屏幕),用于与应用程序启动相关的功能,例如身份验证过程。
- 其他多个 xap 文件可以动态导出,并且视图可以选择性地显示在主内容持有者中。
- 除 ProductApp.Main xap 中的 ViewModel 和 Model 外,都在单独的程序集/项目中。
- 添加任何程序集/项目和模块以扩展应用程序都很容易。
- 仅使用 MVVMLight 库的基本部分进行命令、消息传递和模块清理。避免任何复杂的插件框架,以便于学习和实现。
创建主内容持有者项目
在 Visual Studio 中打开现有的 ProductApp 应用程序时,解决方案中的 Silverlight 服务器和客户端项目如下图所示。
现有应用程序在客户端直接从 ProductApp 项目开始,而该项目在新模式设计中将成为一个可组合的 xap。我们将创建另一个客户端项目 ProductApp.Main,它可以作为主内容持有者(或交换板)运行。它还可以托管一些在应用程序启动阶段运行的模块。我们希望通过修改来重用现有的 ProductApp 客户端项目,以便所有现有引用和一些必需项都可以转移到新项目。
通过从 文件 菜单中选择 导出模板...,然后从下拉列表中选择 ProductApp 项目来导出现有项目的模板。在 导出模板向导 屏幕上使用所有其他默认选择来完成任务。有关导出自定义模板的详细信息,请参阅此文档。
使用标准步骤将新项目 ProductApp.Main 添加到解决方案中,但使用自定义模板 ProductApp。
转到 Web 主机服务器项目 ProductApp.Web。在项目 属性 屏幕的 Silverlight 应用程序 部分,单击 添加,然后在 添加 Silverlight 应用程序 屏幕上,从下拉列表中选择 ProductApp.Main。保持 添加一个引用控制框的测试页 处于选中状态,因为我们需要这个新项目的起始页。
回到 解决方案资源管理器 中的 ProductApp.Main 项目。删除从自定义模板继承的 AddProductWindow.xaml 和 ProductList.xaml 文件。然后将 ErrorWindow.xaml 拖放到 ProductApp.Main 项目根目录。通用错误显示屏幕更多地与 UI 相关,不会更改为 MVVM 和 MEF 可组合模式。
在 ProductApp.Main 项目的 Views 文件夹中添加一个新的 Silverlight 用户控件,命名为 MainPage.xaml。现在,项目的文件夹和文件结构如下所示。
打开 App.xaml.cs 文件,在
Application_Start()
中将ProjectList
更改为MainPage
。稍后我们会将其他代码片段添加到 ProductApp.Main 项目的文件中。
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new Views.MainPage();
}
重构现有的 ProductApp 项目
ProductApp 项目将重命名为 ProductApp.Views,它将作为可组合的 xap 运行。请遵循重命名方法来完成更改。
将 ProductList.xaml 和 AddProductWindow.xaml 从 Views 文件夹拖放到项目根目录。由于此项目只包含视图文件,我们不需要 Views 文件夹来对文件进行分组。
删除仍包含 ErrorWindow.xaml 文件的 Views 文件夹。我们也不需要此项目中的内置错误报告窗口。
启动
App
类(App.xaml 和 App.xaml.cs)以及 Assets 文件夹中的 Styles.xaml 在运行时并未被 ProductApp.Views.xap 作为可组合部分在 ProductApp.Main 上下文中执行。删除项目中的这些文件不会影响应用程序的运行时行为。但是,我们可以将它们保留在那里以供设计时使用,或者如果通过直接启动它来测试程序集。在这种情况下,由于删除了 ErrorWindow.xaml,我们需要替换 App.xaml.cs 文件中用于呈现未处理异常的方法。否则,我们将收到编译错误。由于重要性较低,此处未显示更新的代码。您可以从下载的源包中复制代码,甚至复制 App.xaml 及其 .cs 文件。在 web 主机服务器项目 ProductApp.Web 中,删除 ProductAppTestPage.aspx 和 ProductAppTestPage.html 页面。应用程序将从新的测试页开始。重构后的 ProductApp.Views 客户端项目和 web 主机服务器项目应该如下所示。
保存所有更新的文件,将 ProductAppTestPage.aspx 设置为起始页,然后运行应用程序。ProductApp.Views 项目中的 产品列表 屏幕应与之前相同。
请注意,由于除 App.xaml 和 App.xaml.cs 之外的所有文件都已具有命名空间 ProductApp.Views,因此我们应该只通过在 当前文档 中查找来替换 App.xaml 和 App.xaml.cs 文件中的命名空间 ProductApp 为 ProductApp.Views。此外,请确保 xap 文件名已替换为 ProductApp.Views.xap。
添加公共类库项目
使用 MEF 的基本操作由本项目中的代码执行或调解。
在解决方案中添加一个名为 ProductApp.Common 的新 Silverlight 类库项目。
将这些引用添加到项目中:
- ProductRiaLib (项目)
- System.ComponentModel.Composition (.NET)
- System.ComponentModel.Composition.Initialization (.NET)
- System.ServiceModel.DomainServices.Client (.NET)
删除自动生成的 Class1.cs 文件,然后在项目中创建 Constants 和 ModuleServices 文件夹。文件夹仅用于对相关文件进行分组,以提高清晰度和易于维护。项目中的任何类和接口都在默认根命名空间下,不添加其文件夹名称。
在 Constants 文件夹中添加一个名为 ModuleID.cs 的新类文件。
在 ModuleServices 文件夹中添加三个新的类文件:IModule.cs、IModuleMetadata.cs 和 ModuleCatelogSerive.cs。现在,解决方案资源管理器 中的 ProductApp.Common 项目应该如下所示。
打开 Contants\ModuleID.cs 文件,将其全部替换为以下代码行。这次我们只定义一个模块 ID,用于访问导出的 ProductApp.Views.xap 中的 ProductListView 模块。
将代码添加到 ModuleServices\IModule.cs 并替换任何现有内容。它是最简单的接口,用作模块类型和导出/导入契约。
在 ModuleServices\IModuleMetadata.cs 中输入代码行并替换任何现有代码。我们只需要一个通用的元数据属性
Name
,其值为模块 ID,用于 MEF 导出。在 ModuleServices\ModuleCategoryService.cs 中输入以下代码行并覆盖任何现有代码。此类的成员执行应用程序的核心 MEF 功能。注释标签解释了代码行具体做了什么。这次我们将所有成员都放在类中,尽管有些成员在实现本系列文章后续部分描述的过程之前不会被调用。
将此项目的引用添加到所有 Silverlight 客户端项目。目前,只涉及 ProductApp.Main 和 ProductApp.Views。
保存此项目中的所有更新文件。我们已准备好加载 ProductApp.Views.xap 并从 xap 导出现有的 ProductList 模块以进行显示。
namespace ProductApp.Common
{
public sealed class ModuleID
{
// Product List
public const string ProductListView = "ProductListView";
}
}
namespace ProductApp.Common
{
// Just used as a common type for any exported module.
public interface IModule
{
}
}
namespace ProductApp.Common
{
// A metadata view containing only one property
public interface IModuleMetadata
{
string Name { get; }
}
// Use string Constants as keys for metadata properties
// Not enter string values on the spot
public sealed class MetadataKeys
{
public const string Name = "Name";
}
}
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
namespace ProductApp.Common
{
public class ModuleCatalogService
{
// Define a catalog of the mixed type
private static AggregateCatalog _aggregateCatalog;
// Define the dictionary for loaded xaps
private Dictionary<<string, DeploymentCatalog> _catalogs;
// Define the collections for storing expored modules
private List<ExportLifetimeContext<IModule>> _contextList;
private List<Lazy<IModule>> _lazyList;
// Define the container
public static CompositionContainer Container { get; private set; }
// Expose this service instance
public static ModuleCatalogService Instance { get; private set; }
// Define a collection of ExportFactory object
[ImportMany(AllowRecomposition = true)]
public IEnumerable<ExportFactory<IModule,
IModuleMetadata>> FactoryModules { get; set; }
// Define a collection of Lazy object
[ImportMany(AllowRecomposition = true)]
public IEnumerable<Lazy<IModule, IModuleMetadata>> LazyModules { get; set; }
public ModuleCatalogService()
{
_catalogs = new Dictionary<string, DeploymentCatalog>();
_contextList = new List<ExportLifetimeContext<IModule>>();
_lazyList = new List<Lazy<IModule>>();
// Fill the imports of all parts held by this service instance
CompositionInitializer.SatisfyImports(this);
}
static ModuleCatalogService()
{
_aggregateCatalog = new AggregateCatalog();
//Add an instance of catalog for xap to the Catalog collection
_aggregateCatalog.Catalogs.Add(new DeploymentCatalog());
//Use the aggregate catalog with the container
Container = new CompositionContainer(_aggregateCatalog);
// Initialize the logical composition container
CompositionHost.Initialize(Container);
Instance = new ModuleCatalogService();
}
public static void Initialize()
{
// Any call to this static method will call the static constructor
// the "ModuleCatalogService()"
// It's called firstly from the starting App.xaml.cs
}
public void AddXap(string uri, Action<AsyncCompletedEventArgs> completedAction = null)
{
DeploymentCatalog catalog;
if (!_catalogs.TryGetValue(uri, out catalog))
// Run the code if the xap is not loaded (no data in the dictionary)
{
catalog = new DeploymentCatalog(uri);
// Event handler registered and running for adding the xap
catalog.DownloadCompleted += (s, e) =>
{
if (e.Error == null)
{
// Add the DeploymentCatelog instance to the dictionary.
_catalogs.Add(uri, catalog);
// Add the DeploymentCatelog instance to the Catologs collection
_aggregateCatalog.Catalogs.Add(catalog);
}
else
{
throw new Exception(e.Error.Message, e.Error);
}
};
// Set the event handler and notify the caller
if (completedAction != null)
catalog.DownloadCompleted += (s, e) => completedAction(e);
// Begin to download the xap
catalog.DownloadAsync();
}
else
// Xap has been loaded previously and just notify the caller
// to run the event handler routine
{
if (completedAction != null)
{
AsyncCompletedEventArgs e = new AsyncCompletedEventArgs(null, false, null);
completedAction(e);
}
}
}
public void RemoveXap(string uri)
{
DeploymentCatalog catalog;
if (_catalogs.TryGetValue(uri, out catalog))
{
// Remove the DeploymentCatelog instance in the Catalogs collection
_aggregateCatalog.Catalogs.Remove(catalog);
// Remove the xap from the dictionary
_catalogs.Remove(uri);
}
}
public object GetModule(string moduleId)
{
// An object to hole an instance of ExportLifetimeContext
ExportLifetimeContext<IModule>> context;
// Search for the module by matching the metadata Name property
context = FactoryModules.FirstOrDefault(
n => (n.Metadata.Name == moduleId)).CreateExport();
// Cache the instance of ExportLifetimeContext to the list
_contextList.Add(context);
return context.Value;
}
public object GetModuleLazy(string moduleId)
{
// A Lazy object to hold the exported module
Lazy<IModule, IModuleMetadata> lazyModule;
// Search for the module by matching the metadata Name property
lazyModule = LazyModules.FirstOrDefault(n => n.Metadata.Name == moduleId);
// Cache the instance of the Lazy to the list
_lazyList.Add(lazyModule);
return lazyModule.Value;
}
public bool ReleaseModule(IModule module)
{
// Set module reference back to the context
ExportLifetimeContext<IModule> context =
_contextList.FirstOrDefault(n => n.Value.Equals(module));
if (context == null) return false;
// Remove the module context from the collection
_contextList.Remove(context);
// Calls Cleanup() in the module and then set the module null.
context.Dispose();
context = null;
return true;
}
public bool ReleaseModuleLazy(IModule module)
{
// Set module reference back to the lazyModule
Lazy<IModule> lazyModule = _lazyList.FirstOrDefault(n => n.Value.Equals(module));
if (lazyModule == null) return false;
// Remove the module lazy from the collection
_lazyList.Remove(lazyModule);
// No Dispose() for the Lazy object and call this method of the container
Container.ReleaseExport(lazyModule);
lazyModule = null;
return true;
}
}
}
使 ProductList 类可导出
我们需要将 Export
属性添加到 ProductList 类以使其可组合。有关使用 Export/Import 属性的详细信息,请参阅 MSDN 文档。我们在这里使用公共类型 IModule
作为契约,并且只使用带有模块 ID 作为其值的元数据 Name
属性。模块 ID 对于导出方和导入方之间的通信至关重要。我们不使用自定义属性以避免创建更多接口或类。
将 System.ComponentModel.Composition (.NET) 引用添加到 ProductApp.Views 项目中。
打开 ProductList.xaml.cs 代码隐藏文件。添加或解析两条 using 语句。然后将从命名空间行到类定义行的代码行替换为如下所示。
// - - - Existing using statement lines above
using System.ComponentModel.Composition;
using ProductApp.Common;
namespace ProductApp.Views
{
[Export(typeof(IModule)), ExportMetadata(MetadataKeys.Name, ModuleID.ProductListView)]
public partial class ProductList : UserControl, IModule
{
// - - - other existing code below
加载 Xap 和导出的模块
我们现在可以修改 ProductApp.Main 项目以获取 ProductApp.Views.xap 并导入 ProductList 模块。
打开 ProductApp.Main/App.xaml.cs 文件,并添加代码以初始化目录服务。这将调用
ModuleCategoryService
的构造函数,用于加载用于处理导出/导入的对象并创建组合容器。将 MainPage.xaml 文件中的现有代码替换为以下代码行。这将创建一个带有应用程序带、带上的链接按钮和简单内容控件的屏幕。我们在这里将导航 UI 保持尽可能简单。对于实际应用程序,可以在 MainPage 用户控件上使用任何其他元素和样式,例如菜单或选项卡控件。
将 MainPage.xaml.cs 代码隐藏中的现有代码替换为以下代码片段。此时,我们仍然使用代码隐藏来加载 xap 文件并导入模块。我们将在本系列文章的下一部分中将应用程序更新为可组合的 MVVM 模式。
运行应用程序。从已加载的 ProductApp.Views.xap 中导出的 ProductList 模块应导出到组合容器并显示在屏幕上。
private void Application_Startup(object sender, StartupEventArgs e)
{
// Initilizing the catelog service on the application start
ProductApp.Common.ModuleCatalogService.Initialize();
this.RootVisual = new Views.MainPage();
}
<UserControl x:Class="ProductApp.Main.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot"
Style="{StaticResource LayoutRootGridStyle}">
<Grid x:Name="NavigationGrid"
Style="{StaticResource NavigationGridStyle}">
<Border x:Name="BrandingBorder"
Style="{StaticResource BrandingBorderStyle}">
<StackPanel x:Name="BrandingStackPanel"
Style="{StaticResource BrandingStackPanelStyle}">
<TextBlock x:Name="ApplicationNameTextBlock"
Style="{StaticResource ApplicationNameStyle}"
Text="Demo Product Application" />
</StackPanel>
</Border>
<Border x:Name="LinksBorder"
Style="{StaticResource LinksBorderStyle}">
<StackPanel x:Name="LinksStackPanel"
Style="{StaticResource LinksStackPanelStyle}">
<HyperlinkButton x:Name="linkButton_ProductList"
Style="{StaticResource LinkStyle}"
Content="Product List"
Click="LinkButton_Click" />
</StackPanel>
</Border>
</Grid>
<ScrollViewer x:Name="PageScrollViewer"
Style="{StaticResource PageScrollViewerStyle}"
Margin="0,41,0,0">
<StackPanel x:Name="ContentStackPanel">
<ContentControl x:Name="MainContent"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" />
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;
using ProductApp.Common;
namespace ProductApp.Main.Views
{
public partial class MainPage : UserControl
{
private ModuleCatalogService _catalogService = ModuleCatalogService.Instance;
public MainPage()
{
InitializeComponent();
}
// Still use code-behind for test MEF before MVVM implementation.
private void LinkButton_Click(object sender, RoutedEventArgs e)
{
// Call to load the xap
string xapUri = "/ClientBin/ProductApp.Views.xap";
_catalogService.AddXap(xapUri, arg => ProductApp_OnXapDownloadCompleted(arg));
}
private void ProductApp_OnXapDownloadCompleted(AsyncCompletedEventArgs e)
{
// Inject the module after loading xap completed
MainContent.Content = _catalogService.GetModule(ModuleID.ProductListView);
// UI - set active link
VisualStateManager.GoToState(linkButton_ProductList, "ActiveLink", true);
}
}
}
摘要
在本系列文章的这一部分中,我们已开始更改 Silverlight 演示应用程序的模式。我们提出了架构设计,执行了项目结构更新,并完成了 xap 文件加载和模块导出的代码。我们将在本系列文章的第 2 部分中将应用程序转换为使用 MVVM 模式,并创建更多具有 MVVM 结构的 MEF 可组合模块。