65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight 应用程序从基础迁移到 MVVM 和 MEF 可组合模式 - 第 1 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (3投票s)

2012年5月3日

CPOL

9分钟阅读

viewsIcon

25519

downloadIcon

714

本文系列展示了如何通过简单的方法和详细的代码解释,将具有基本模式的Silverlight应用程序升级为MVVM和MEF可组合模式。

引言

我之前的一篇文章展示了一个简单的演示应用程序,该应用程序使用 WCF RIA Services 类库和代码优先的领域数据服务进行 CRUD 数据操作。它有一个主屏幕和一个带有基本导航和代码隐藏模式的子窗口。如果我们将应用程序升级到具有 MVVM 和 MEF 可组合模式的应用程序,会发生什么?这些方法有多容易?编码的细节是什么?本系列文章将通过最简单的方法和详细的编码解释来解决这些问题。完成后的应用程序不会是一个成熟的示例,但应包括 MVVM 和 MEF 可组合模式实现的所有主要方面,而不关注其他一些领域,例如 UI、数据验证、数据服务操作或安全性。我还将描述如何使用新模式处理弹出子窗口,执行可组合部件清理,以及在纯 MVVM 样式中切换屏幕时持久化状态。

目录

架构简介

现有的演示应用程序结构简单。类直接引用,所有业务逻辑处理代码片段都在代码隐藏分部类中。

11.png

当应用程序更新到 MVVM 和 MEF 可组合模式时,可组合部件(在本系列文章中也称为模块)被添加,现有部件也重构为模块,如下图所示。

12.png

该图表示以下功能。

  • ProductApp.Main.xap 中的视图被设置为主内容持有者。
  • xap 程序集可以有其他 MVVM 集(在本例中为另一个屏幕),用于与应用程序启动相关的功能,例如身份验证过程。
  • 其他多个 xap 文件可以动态导出,并且视图可以选择性地显示在主内容持有者中。
  • ProductApp.Main xap 中的 ViewModel 和 Model 外,都在单独的程序集/项目中。
  • 添加任何程序集/项目和模块以扩展应用程序都很容易。
  • 仅使用 MVVMLight 库的基本部分进行命令、消息传递和模块清理。避免任何复杂的插件框架,以便于学习和实现。

创建主内容持有者项目

在 Visual Studio 中打开现有的 ProductApp 应用程序时,解决方案中的 Silverlight 服务器和客户端项目如下图所示。

13.png

现有应用程序在客户端直接从 ProductApp 项目开始,而该项目在新模式设计中将成为一个可组合的 xap。我们将创建另一个客户端项目 ProductApp.Main,它可以作为主内容持有者(或交换板)运行。它还可以托管一些在应用程序启动阶段运行的模块。我们希望通过修改来重用现有的 ProductApp 客户端项目,以便所有现有引用和一些必需项都可以转移到新项目。

  1. 通过从 文件 菜单中选择 导出模板...,然后从下拉列表中选择 ProductApp 项目来导出现有项目的模板。在 导出模板向导 屏幕上使用所有其他默认选择来完成任务。有关导出自定义模板的详细信息,请参阅此文档

  2. 使用标准步骤将新项目 ProductApp.Main 添加到解决方案中,但使用自定义模板 ProductApp

  3. 14.png

  4. 转到 Web 主机服务器项目 ProductApp.Web。在项目 属性 屏幕的 Silverlight 应用程序 部分,单击 添加,然后在 添加 Silverlight 应用程序 屏幕上,从下拉列表中选择 ProductApp.Main。保持 添加一个引用控制框的测试页 处于选中状态,因为我们需要这个新项目的起始页。

  5. 15.png

  6. 回到 解决方案资源管理器 中的 ProductApp.Main 项目。删除从自定义模板继承的 AddProductWindow.xamlProductList.xaml 文件。然后将 ErrorWindow.xaml 拖放到 ProductApp.Main 项目根目录。通用错误显示屏幕更多地与 UI 相关,不会更改为 MVVM 和 MEF 可组合模式。

  7. 16.png

  8. ProductApp.Main 项目的 Views 文件夹中添加一个新的 Silverlight 用户控件,命名为 MainPage.xaml。现在,项目的文件夹和文件结构如下所示。

  9. 17.png

  10. 打开 App.xaml.cs 文件,在 Application_Start() 中将 ProjectList 更改为 MainPage

  11. private void Application_Startup(object sender, StartupEventArgs e)
    {
        this.RootVisual = new Views.MainPage();
    }
  12. 稍后我们会将其他代码片段添加到 ProductApp.Main 项目的文件中。

重构现有的 ProductApp 项目

  1. ProductApp 项目将重命名为 ProductApp.Views,它将作为可组合的 xap 运行。请遵循重命名方法来完成更改。

  2. 请注意,由于除 App.xamlApp.xaml.cs 之外的所有文件都已具有命名空间 ProductApp.Views,因此我们应该只通过在 当前文档 中查找来替换 App.xamlApp.xaml.cs 文件中的命名空间 ProductAppProductApp.Views。此外,请确保 xap 文件名已替换为 ProductApp.Views.xap

  3. ProductList.xamlAddProductWindow.xamlViews 文件夹拖放到项目根目录。由于此项目只包含视图文件,我们不需要 Views 文件夹来对文件进行分组。

  4. 删除仍包含 ErrorWindow.xaml 文件的 Views 文件夹。我们也不需要此项目中的内置错误报告窗口。

  5. 启动 App 类(App.xaml 和 App.xaml.cs)以及 Assets 文件夹中的 Styles.xaml 在运行时并未被 ProductApp.Views.xap 作为可组合部分在 ProductApp.Main 上下文中执行。删除项目中的这些文件不会影响应用程序的运行时行为。但是,我们可以将它们保留在那里以供设计时使用,或者如果通过直接启动它来测试程序集。在这种情况下,由于删除了 ErrorWindow.xaml,我们需要替换 App.xaml.cs 文件中用于呈现未处理异常的方法。否则,我们将收到编译错误。由于重要性较低,此处未显示更新的代码。您可以从下载的源包中复制代码,甚至复制 App.xaml 及其 .cs 文件。

  6. 在 web 主机服务器项目 ProductApp.Web 中,删除 ProductAppTestPage.aspxProductAppTestPage.html 页面。应用程序将从新的测试页开始。重构后的 ProductApp.Views 客户端项目和 web 主机服务器项目应该如下所示。

  7. 18.png

  8. 保存所有更新的文件,将 ProductAppTestPage.aspx 设置为起始页,然后运行应用程序。ProductApp.Views 项目中的 产品列表 屏幕应与之前相同。

添加公共类库项目

使用 MEF 的基本操作由本项目中的代码执行或调解。

  1. 在解决方案中添加一个名为 ProductApp.Common 的新 Silverlight 类库项目。

  2. 将这些引用添加到项目中:

    • ProductRiaLib (项目)
    • System.ComponentModel.Composition (.NET)
    • System.ComponentModel.Composition.Initialization (.NET)
    • System.ServiceModel.DomainServices.Client (.NET)
  3. 删除自动生成的 Class1.cs 文件,然后在项目中创建 ConstantsModuleServices 文件夹。文件夹仅用于对相关文件进行分组,以提高清晰度和易于维护。项目中的任何类和接口都在默认根命名空间下,不添加其文件夹名称。

  4. Constants 文件夹中添加一个名为 ModuleID.cs 的新类文件。

  5. ModuleServices 文件夹中添加三个新的类文件:IModule.cs、IModuleMetadata.csModuleCatelogSerive.cs。现在,解决方案资源管理器 中的 ProductApp.Common 项目应该如下所示。

  6. 19.png

  7. 打开 Contants\ModuleID.cs 文件,将其全部替换为以下代码行。这次我们只定义一个模块 ID,用于访问导出的 ProductApp.Views.xap 中的 ProductListView 模块。

  8. namespace ProductApp.Common
    {
        public sealed class ModuleID
        {
            // Product List
            public const string ProductListView = "ProductListView";
        }
    }
  9. 将代码添加到 ModuleServices\IModule.cs 并替换任何现有内容。它是最简单的接口,用作模块类型和导出/导入契约。

  10. namespace ProductApp.Common
    {    
        // Just used as a common type for any exported module.
        public interface IModule
        {         
        }
    }
  11. ModuleServices\IModuleMetadata.cs 中输入代码行并替换任何现有代码。我们只需要一个通用的元数据属性 Name,其值为模块 ID,用于 MEF 导出。

  12. 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";
        }
    }
  13. ModuleServices\ModuleCategoryService.cs 中输入以下代码行并覆盖任何现有代码。此类的成员执行应用程序的核心 MEF 功能。注释标签解释了代码行具体做了什么。这次我们将所有成员都放在类中,尽管有些成员在实现本系列文章后续部分描述的过程之前不会被调用。

  14. 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;
                }
            }
        }
  15. 将此项目的引用添加到所有 Silverlight 客户端项目。目前,只涉及 ProductApp.MainProductApp.Views

  16. 保存此项目中的所有更新文件。我们已准备好加载 ProductApp.Views.xap 并从 xap 导出现有的 ProductList 模块以进行显示。

使 ProductList 类可导出

我们需要将 Export 属性添加到 ProductList 类以使其可组合。有关使用 Export/Import 属性的详细信息,请参阅 MSDN 文档。我们在这里使用公共类型 IModule 作为契约,并且只使用带有模块 ID 作为其值的元数据 Name 属性。模块 ID 对于导出方和导入方之间的通信至关重要。我们不使用自定义属性以避免创建更多接口或类。

  1. System.ComponentModel.Composition (.NET) 引用添加到 ProductApp.Views 项目中。

  2. 打开 ProductList.xaml.cs 代码隐藏文件。添加或解析两条 using 语句。然后将从命名空间行到类定义行的代码行替换为如下所示。

  3. // - - - 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 模块。

  1. 打开 ProductApp.Main/App.xaml.cs 文件,并添加代码以初始化目录服务。这将调用 ModuleCategoryService 的构造函数,用于加载用于处理导出/导入的对象并创建组合容器。

  2. 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();
    }
  3. MainPage.xaml 文件中的现有代码替换为以下代码行。这将创建一个带有应用程序带、带上的链接按钮和简单内容控件的屏幕。我们在这里将导航 UI 保持尽可能简单。对于实际应用程序,可以在 MainPage 用户控件上使用任何其他元素和样式,例如菜单或选项卡控件。

  4. <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>
  5. MainPage.xaml.cs 代码隐藏中的现有代码替换为以下代码片段。此时,我们仍然使用代码隐藏来加载 xap 文件并导入模块。我们将在本系列文章的下一部分中将应用程序更新为可组合的 MVVM 模式。

  6. 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);
            }
        }
    }
  7. 运行应用程序。从已加载的 ProductApp.Views.xap 中导出的 ProductList 模块应导出到组合容器并显示在屏幕上。

  8. 20.png

摘要

在本系列文章的这一部分中,我们已开始更改 Silverlight 演示应用程序的模式。我们提出了架构设计,执行了项目结构更新,并完成了 xap 文件加载和模块导出的代码。我们将在本系列文章的第 2 部分中将应用程序转换为使用 MVVM 模式,并创建更多具有 MVVM 结构的 MEF 可组合模块。

© . All rights reserved.