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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2012年5月3日

CPOL

12分钟阅读

viewsIcon

25795

downloadIcon

554

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

引言

完成本文系列前几部分所述的工作后,我们已经创建了主内容持有者项目,并将“产品列表”屏幕及其子窗口从基本模式升级为MVVM和MEF可组合模式。在本文中,我们将向“ProductApp.Main”项目添加另一个演示屏幕,并在解决方案中创建另一组项目以用于新的xap程序集,以便我们可以在导出的模块和xap程序集之间切换屏幕。然后,我们将实现模块清理过程,并向应用程序添加状态持久化功能。

目录和链接

向主客户端项目添加另一个屏幕

此屏幕提供了一个在同一项目以及未导出的xap程序集中实现可组合MVVM的示例。为方便演示,我们将保持此屏幕的模块尽可能简单。任何必需的模块ID字符串常量已在“ProductApp.Common\Constants\ModuleID.cs”文件中。

  1. 在“ProductApp.Common”项目中,向“Model”文件夹添加一个接口文件“IOtherModel”。它将用作新ViewModel和Model之间通信的契约。为了演示文本,仅定义了一个字符串属性。

  2. namespace ProductApp.Common
    {
        public interface IOtherModel 
        {
            string DemoTextData { get; set; }
        }
    }
  3. 在“ProductApp.Main”项目中,创建一个新文件夹“Models”,然后向该文件夹添加一个新类文件“AnotherScreenModel.cs”。为类添加简单的代码。请注意,这次我们将导出的模块定义为非共享模块的示例。

  4. 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;}                              
        }                                             
    }
  5. 在同一项目中,将“ViewModels”文件夹中的“AnotherScreenViewModel.cs”文件添加到“ViewModels”文件夹中,并将以下代码放入类中。请注意,我们分别定义了Lazy对象及其Value的变量。Lazy对象的Value属性将设置为类/模块实例,而当离开屏幕时,将清理Lazy对象本身,而不是其Value

  6. 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");
                    }
                }
            }                      
        }
    }
  7. 向“ProductApp.Main”项目的“Views”文件夹添加一个名为“AnotherScreen.xaml”的新的Silverlight用户控件,然后在xaml文件的Grid节点下添加一个TextBlock

  8. <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”的文件夹和文件结构如下所示。

    31.png

  9. 打开“MainPageViewModels.cs”,并将以下代码块添加到OnLoadModuleCommand方法的switch语句中。在接收到导航命令后,ViewModel会向View发送一条消息以加载请求的屏幕。

  10. // Send message back to MainPageView code-behind to load AnotherScreenView
    case ModuleID.AnotherScreenView:
        Messenger.Default.Send(ModuleID.AnotherScreenView, MessageToken.LoadScreenMessage);
        _currentViewText = ModuleID.AnotherScreenView;
        break;
  11. 打开“MainPage.xaml.cs”,并将以下代码块添加到OnLoadScreenMessage方法的switch语句中,以便将“AnotherScreen.xaml”View加载到内容持有者中。

  12. case ModuleID.AnotherScreenView:
        newScreen = _catalogService.GetModule(ModuleID.AnotherScreenView);
        break;
  13. 在“MainPage.xaml”文件中,在第一个HyperlinkButton下方添加第二个HyperlinkButton

  14. <Rectangle x:Name="Divider1" Style="{StaticResource DividerStyle}" />
    <HyperlinkButton x:Name="linkButton_AnotherScreen" 
                Style="{StaticResource LinkStyle}" Content="Another Screen"
                Command="{Binding Path=LoadModuleCommand}" 
                CommandParameter="AnotherScreenView" />
  15. 运行应用程序。点击“Another Screen”链接按钮在浏览器中打开屏幕。现在可以执行导出模块之间的切换。

  16. 32.png

向应用程序添加另一个Xap

完成了前面的工作,向解决方案添加项目以创建新的xap程序集并不困难。我们将使用快捷方式创建xap,并使MVVM模块类似于“Another Screen”链接的模块。

  1. 在解决方案中创建一个虚拟文件夹“AnotherXap.Client”。

  2. 在下一步创建自定义模板之前,让我们将现有“ProductApp.Client”虚拟文件夹下的“ProductApp.Views”、“ProductApp.ViewModels”和“ProductApp.Models”项目中几乎所有引用的“Copy Local”设置为“False”。“ProductApp.Main”项目会将所有引用的dll文件加载到其bin文件夹中,这些文件可供应用程序中的所有其他客户端项目使用,因此其他程序集不需要在其自己的本地bin文件夹中包含相同的副本。这将减小xap及其引用的ViewModel/Model程序集的大小,从而加快加载速度。使用自定义模板的好处是,现有项目中的所有引用设置将自动应用到新项目中。

  3. 在“ProductApp.Views”项目中,选择除“ProductApp.Models”和“ProductApp.ViewModels”之外的所有引用,右键单击打开“Properties”面板,然后在“Copy Local”项目的下拉列表中选择“False”。

    33.png

    对“ProductApp.ViewModel”和“ProductApp.Model”也执行相同的操作,但选择其中的所有引用项。

  4. 从现有“ProductApp.Client”虚拟文件夹下的这三个项目中导出自定义模板,然后使用相应的自定义模板在“AnotherXap.Client”虚拟文件夹下创建三个新项目。新项目的名称分别为“AnotherXap.Views”、“AnotherXap.ViewModels”和“AnotherXap.Models”。

  5. 在“AnotherXap.Models”项目中执行更改。

    • 删除项目根文件夹中的类文件。
    • 将“ProductApp.Main”项目中的“Models\AnotherScreenModel.cs”复制/粘贴到当前项目中,并将其重命名为“AnotherXapModel.cs”。
    • 将代码中的命名空间“ProductApp.Main.Models”重命名为“AnotherXap.Models”。
    • 将当前文档中的所有“AnotherScreen”实例替换为“AnotherXap”。
    • 将“DemoTextData”属性的值替换为“IT'S THE MODULE FORM ANOTHER ZAP”或其他内容。
  6. 在“AnotherXap.ViewModels”项目中执行类似的更改。

    • 删除项目根文件夹中的类文件。
    • 将“ProductApp.Main”项目中的“ViewModels\AnotherScreenViewModel.cs”复制/粘贴到当前项目中,并将其重命名为“AnotherXapViewModel.cs”。
    • 将代码中的命名空间“ProductApp.Main.ViewModels”重命名为“AnotherXap.ViewModels”。
    • 将当前文档中的所有“AnotherScreen”实例替换为“AnotherXap”。
  7. 在“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”。

    “AnotherXap.Client”虚拟文件夹下的项目和文件如下图所示。

    34.png

  8. 打开“App.xaml.cs”,并在Application_Startup方法中将从模板继承的“ProductList”代码替换为“AnotherXap”代码。

  9. 在“AnotherXap.Views”项目中,删除从模板继承的“ProductApp.ViewModels”和“ProductApp.Models”的旧引用。然后向当前项目添加新项目“AnotherXap.ViewModels”和“AnotherXap.Models”的引用。

  10. 打开“MainPageViewModels.cs”,并将以下代码块添加到OnLoadModuleCommand方法的switch语句中。

  11. // 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;
    }
  12. 打开“MainPage.xaml.cs”,并将以下代码块添加到OnLoadScreenMessage方法的switch语句中,以便将“AnotherXap”View加载到内容持有者中。

  13. case ModuleID.AnotherXapView:
        newScreen = _catalogService.GetModule(ModuleID.AnotherXapView);
        break;
  14. 在“MainPage.xaml”文件中,在现有HyperlinkButton下方再添加一个HyperlinkButton

  15. <Rectangle x:Name="Divider2" Style="{StaticResource DividerStyle}" />
    <HyperlinkButton x:Name="linkButton_AnotherXap" 
                Style="{StaticResource LinkStyle}" Content="Another Xap"
                Command="{Binding Path=LoadModuleCommand}" 
                CommandParameter="AnotherXapView" />
  16. 在Web托管服务器“ProductApp.Web”项目中,在项目“Properties”页的“Silverlight Application”选项卡上,使用我们之前添加“ProductApp.Main”的方法,从现有下拉列表中将“AnotherXap.Views”项目添加到Web托管服务器项目中。但这次不需要生成起始测试页面。

  17. 35.png

  18. 运行应用程序以测试三个链接按钮和屏幕内容。

  19. 36.png

清理非共享模块

MEF可组合应用程序中一个值得注意的问题是组合容器和导出模块的处置。如果处理不当,会导致内存泄漏。对于像本演示应用程序这样的单容器应用程序,容器的处置不是问题。容器的生命周期是用户会话。共享模块也会一直保留到用户关闭应用程序。我们专注于非共享模块的清理任务。以下是基本规则和工作流程。

  • 不再需要的模块的清理从请求的新View模块准备加载的那一刻开始。
  • 清理顺序是Model、ViewModel和View。
  • 任何模块都将其自身成员设置为null,然后通过下一级模块的调用来清理该模块。
  • 拥有任何子模块或嵌入式模块的模块负责首先清理其下属模块。

在本演示应用程序中,我们将为所有模块放置清理代码,除了“MainPage”(内容持有者)的View和ViewModel以及“ProductListModel”(共享模块)。由于相似性,我仅展示如何为与“Product List”屏幕相关的模块以及需要特殊解释的其他模块编写清理过程。下载的源代码包包含所有模块的完成代码。

  1. 将以下代码块添加到“MainPage.xaml.cs”中OnLoadScreenMessage方法switch语句块的紧后面。

  2. // 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);
    }
  3. 转到“ProductApp.Views”项目,在“ProductList.xaml.cs”中为“ProductList”类添加ICleanup到继承列表中。请注意,除“MainPage.xaml.cs”外,所有View代码隐藏模块都应实现MVVMLight中的ICleanup接口。

  4. 将下面显示的Cleanup方法添加到“ProductList.xaml.cs”代码隐藏中。该代码首先调用其子窗口模块中的Cleanup方法,然后调用目录服务释放“ProductListViewModel”模块。ModuleCatalogService类中的ReleaseModule方法会在释放ViewModel之前自动调用ViewModel模块中的Cleanup方法。对于子窗口模块,也会发生类似的清理工作流程。

  5. 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);
    }
  6. 将下面的Cleanup方法添加到“ProductListViewModel.cs”中,它将覆盖MVVMLightViewModelBase类中的同名方法。

  7. 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();
    }
  8. 从下载的源代码包中复制“AddProductWindow.xaml.cs”和“AddProductWindowViewModel.cs”的Cleanup方法,并将代码添加到类中。这将完成“Product List”工作流程的清理代码。

  9. 对剩余部分的清理几乎相同。对于“Another Screen”和“Another Xap”屏幕的模型模块的清理,可能需要提及一个特别的说明。

  10. 打开“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();
    }
  11. 在任何View或ViewModel的Cleanup方法,或ModuleCatalogService.ReleaseModule方法中设置断点。在调试模式下运行应用程序,然后切换屏幕。您可以使用Visual Studio中的“Autos”、“Locals”或“Quick Watch”窗口查看对象和值的清理过程和结果。

在可组合MVVM屏幕之间持久化状态

如果应用程序未维护状态,用户经常会问“当我回到之前的屏幕时,我的东西去哪儿了?”。状态数据可以存储在数据库中,并在重新加载到之前的屏幕时检索。但是,将状态数据保留在应用程序的本地缓存中,便于在用户会话期间持久化状态,这通常足以满足用户需求。对象缓存通常可以放置在用户身份验证上下文中,但为了演示目的,我们将把状态数据存储在用户会话期间持续存在的“MainPageViewModel”上下文中。我们还将使用消息传递方法,在MVVM环境中以完全解耦的方式传输数据。

  1. 在“ProductApp.Common”项目中添加一个名为“SaveState”的新文件夹,然后在该文件夹中添加两个新类文件,“StateCache.cs”和“StateCacheList.cs”。

  2. 37.png

  3. “StateCache.cs”文件中的代码行应如下所示。

  4. 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”中的代码也很简单。具有强类型StateCacheStateCacheList类只有一个空构造函数。

    using System.Collections.Generic;
    
    namespace ProductApp.Common
    {
        public class StateCacheList<T> : List<T> where T : StateCache
        {
            public StateCacheList()
            {
            }       
        }
    }
  5. 打开“MainPageViewModel.cs”,并将代码行添加到私有变量声明部分下方并重写构造函数。我们使用MVVMLight设置了两个通知消息回调处理程序,用于获取和设置状态数据。

  6. 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;
            }
        }
    }
  7. 打开“ProductListViewModel.cs”文件,在构造函数中添加以下代码,以便在打开“Product List”屏幕时请求状态数据。

  8. // 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);
    }
  9. 现在,在离开当前屏幕之前调用SaveStateForMe方法的最佳位置是什么?我们可以通过EventTriggerRelayCommand将“ProductList”View的Unloaded事件处理程序传递给ViewModel来调用该方法。但是,我们已经有了在屏幕关闭前执行的Cleanup例程。我们可以从ViewModel的Cleanup方法中调用以发送保存状态的消息。

  10. public override void Cleanup()
    {
        // Call for saving state info
        SaveStateForMe();
    
        // - - - Remaining code lines...            
    }

    请注意,所有与保存和接收状态数据相关的过程和通信仅在不同程序集的不同ViewModel模块之间以完全解耦的方式发生。当向应用程序添加需要状态持久化的新屏幕时,我们只需要将代码片段添加到新屏幕的ViewModel中。

  11. 运行应用程序并打开“Product List”屏幕。在切换到任何其他屏幕之前,选择一个类别并添加或编辑/保存一些项目。从“Another Screen”或“Another Xap”屏幕返回到“Product List”屏幕,之前显示的数据应该仍然在那里。

  12. 38.png

摘要

基于本文系列三部分的插图,我们使用这些过程将Silverlight演示应用程序从基本模式迁移到MVVM和MEF可组合模式。我们还使用可组合MVVM向应用程序添加了一些新功能。架构设计和代码实现的方法可以轻松扩展并应用于实际的Silverlight业务应用程序。希望本文系列对对此主题感兴趣的开发者有所帮助。

参考文献

  1. Attributes Programming Model Overview (MSDN 文档)
  2. Building Composable Apps in .NET 4 with the Managed Extensibility Framework (Glenn Block, MSDN Magazine)
  3. A Pluggable Architecture for Building Silverlight Applications with MVVM (Weidong Shen, codeproject.com)
  4. Using the MVVM Pattern in Silverlight Applications (Microsoft Silverlight Team, silverlight.net)
  5. MVVMLight Messages (rongchaua.net)
© . All rights reserved.