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

复合应用程序重载

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (38投票s)

2010年11月2日

CPOL

12分钟阅读

viewsIcon

127767

downloadIcon

1476

一个更简单的复合应用程序库。

引言

在我开始一个新项目时,我正在寻找一个库来帮助我编写一个复合应用程序,即一个具有主 Shell(窗口)和可动态添加的可插入扩展(DLL/模块)的应用程序。一个像 Prism 这样的库,但希望更简单。

拼图的许多碎片已经可以在别处找到。应用程序必须在数据和视图之间有清晰的分离,即 MVVM 方法。服务必须与类似 MEF 的东西自动链接。数据验证应该是自动的(感谢 ValidationAttribute)。

但是,在断开消息传递和视图解析方面需要改进。关于视图解析,即查找代表给定业务数据对象的合适视图的过程,我想用一个属性来标记视图,比如 DataView(typeof(BusinessData1)),然后让库来处理其余的事情。这就是这个库的由来。

有什么新功能?

性能改进(在 Composition.GetView() 中)、命令简化、POCO 绑定、WP7 支持,以及 WP7 的 MEF。这是 CodePlex 项目

目录

示例

为了测试我的库是否达到了目标,我将其移植了三个示例。在所有情况下,我都能够减小应用程序大小并保持功能。

  • Josh Smith 的 MVVM 演示。这是最好的示例,因为它小巧简单,但(经过一些修改后)涵盖了库的几乎所有功能,并且是一个真正的复合应用程序。我能够摆脱手动编写的验证代码,而是使用 ValidationAttribute。我调整了 MainWindowApp 类以使其成为复合应用程序,并在 TabItem 中使用 DataControl 来将多个控件绑定到具有不同视图的同一模型。
  • Prism 的主示例,StockTraderApp 项目(庞大的示例)。我删除了 presenters(用于绑定视图和视图模型,现在已替换为对 Composition.GetView()DataControl 的调用)、EventAggregator 和自定义 prism 事件(被 Notifications 静态方法替换)。最具挑战性和有趣的部分是摆脱 RegionManager 并用 IShellView 替换它,后者显式公开了可用的 Shell 区域,并摆脱了 RegionManager 的魔术字符串方法。
  • MEFedMVVM 库演示。该应用程序相对简单,但它广泛使用设计时支持,并且设计时体验非常出色。

单元测试说明了最重要的功能(即 CompositionNotificationViewModelBaseCommand)是如何工作的。

关于 MEF 和 MVVM

Josh Smith 已经在 MSDN 上广泛讨论了 MVVM。但总而言之,MVVM 是一种视图模型方法,其中所有逻辑和信息都包含在模型中。所有,我的意思是所有,甚至包括列表的选定元素或插入符号的位置,如果需要的话。

在 MVVM 中,视图只不过是一些声明性的 XAML(以及可能的 UI 特定代码,如果需要的话,不包含任何业务代码,只有纯 UI 逻辑)。而且,由于业务数据可能无法表达视图中的所有信息(例如选定区域、文本框中的错误值等),业务模型可能会被包装在视图模型中。视图模型是业务模型包装器,具有一些额外的、对视图友好的属性。这提供了多种优势,包括提高可测试性、更好地分离关注点,以及使业务数据和 UI 能够由独立团队处理的可能性。

MEF,即托管扩展框架,以一种非常简洁的方式解决了传递服务和其他共享对象的问题。它还可以轻松查找接口实现。基本上,“消费者”对象使用 Import 属性声明它们需要什么,如下所示

[Import(typeof(ISomeInterface)]
public ISomeInterface MySome { get; set; }

在代码的其他地方,导出对象使用 Export 属性声明。

[Export(typeof(ISomeInterface))]
public class SomeInterfaceImplentation : ISomeInterface
{ /** */ }

注意:属性和方法都可以导出。Import 可以是单个对象(Import)或多个(ImportMany)。我强烈建议您阅读 MEF 文档。要找到对象所需的所有导入的实现,需要执行两个操作

  1. 在程序开始时,应该从类型、程序集和/或目录的列表中初始化 MEF 的类型的“目录”,MEF 将在那里查找导出的位置。在这里,您可以选择感兴趣的模块。使用此库,您将调用方法 Composition.Register(),如下所示。
  2. 您可以“组合”需要解析的对象(即包含导入的对象)。使用此库,您将使用方法 Composition.Compose()

MEF 包含各种标签来控制实例是否共享,以及一个导出的多个实现是否有效。同样,这在 文档 中也有介绍。

复合应用程序是什么样的

复合应用程序是指有一个众所周知的顶级 UI 元素,通常是桌面应用程序的窗口或 Silverlight 的页面。这个顶级 UI 元素称为“Shell”。

Shell 包含多个区域,用于托管可插入的内容。这些内容不是由 Shell 定义的,而是由动态加载的模块(例如,通过 MEF)定义的。

例如,在下面的示例中,Shell 定义了两个区域

  • 一个列表框,显示单个文档列表。
  • 一个 ItemsControl(一个 TabControl),可以包含许多项。
<Window 
    x:Class="DemoApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListBox
            Grid.Column="0"
            Items="{Binding Documents}"
            Header="Documents"
            />
        <TabControl 
            Grid.Column="1"
            IsSynchronizedWithCurrentItem="True" 
            ItemsSource="{Binding Workspaces}" 
            ItemContainerStyle="{StaticResource ClosableTabItemStyle}"/>
    </ Grid >
</Window>

注意:创建 Shell 时不要过于担心;如果需要,在开发后期添加或移动区域相对容易。但移除正在使用的区域则更困难!

一旦定义了 Shell,就应该通过公共库公开一个接口。它可以是 IShellInterface(例如,参见 StockTraderApp 中的 IShellView)或其视图模型(例如,参见 DemoApp 中的 MainViewModel),或两者的组合!

例如,这是 StockTraderApp 示例中的 IShellView 接口

public enum ShellRegion
{
    MainToolBar,
    Secondary,
    Action,
    Research,
    // this will set the currently selected item
    Main,
}
public interface IShellView
{
    void ShowShell(); // this will show the shell window
    void Show(ShellRegion region, object data);
    ObservableCollection<object> MainItems { get; }
}

一旦定义了 Shell,就可以编写复合应用程序。这涉及四个简短的步骤

  1. 定义要动态加载的 DLL
  2. 创建 Shell(或跳过并在 3 中导入)
  3. 导出 Shell 并导入模块
  4. 启动应用程序/初始化模块

例如,StockTraderApp 简化的 App 代码可能如下所示

public partial class App
{
    public App()
    {
        // 1. Opt-in for the DLL of interest (for import-export resolution)
        Composition.Register(
            typeof(Shell).Assembly
            , typeof(IShellView).Assembly
            , typeof(MarketModule).Assembly
            , typeof(PositionModule).Assembly
            , typeof(WatchModule).Assembly
            , typeof(NewsModule).Assembly
            );
        // 2. Create the shell
        Logger = new TraceLogger();
        Shell = new Shell();
    }
 
    [Export(typeof(IShellView))]
    public IShellView Shell { get; internal set; }
 
    [Export(typeof(ILoggerFacade))]
    public ILoggerFacade Logger { get; private set; }
 
    [ImportMany(typeof(IShellModule))]
    public IShellModule[] Modules { get; internal set; }
 
    public void Run()
    {
        // 3. export the shell, import the modules
        Composition.Compose(this);
        Shell.ShowShell();
 
        // 4. Start the modules, they would import the shell
        // and use it to appear on the UI
        foreach (var m in Modules)
            m.Init();
    }
}

核心功能

该库从最初的简陋版本发展壮大了很多。它由两大部分组成。对 MVMM 和复合开发至关重要的功能,以及有用的可选功能。

该库大部分功能的核心类是 Composition 类。它还包含两个重要属性,CatalogContainer,MEF 使用它们来解析导入和导出。您需要在应用程序开始时用 Composition.Register() 填充 Catalog,例如

static App() // init catalog in App’s static constructor
{
    Composition.Register(
        typeof(MapPage).Assembly
        , typeof(TitleData).Assembly
        , typeof(SessionInfo).Assembly
    );
}

稍后,可以通过调用 Composition.Compose() 使用 MEF 来解析服务导入和导出。

Composition GetView

当遵循 MVVM 开发模式时,我们编写业务模型和/或视图模型以及这些数据模型的视图。通常,这些视图只包含“XAML 代码”,并且它们的 DataContext 属性就是业务模型。通常,MVVM 辅助库会提供一些查找和加载这些视图的方法。

在此库中,视图需要使用 DataViewAttribute 进行标记,该属性指定此视图适用于哪种模型类型

[DataView(typeof(CustomerViewModel))]
public partial class CustomerView : UserControl
{
    public CustomerView()
    {
        InitializeComponent();
    }
}

然后,从数据模型,您可以通过调用 Composition.GetView() 自动加载相应的视图(并设置其 DataContext),例如

public void ShowPopup(object message, object title)
{
    var sDialog = new MsgBox();
    sDialog.Message = Composition.GetView(message);
    sDialog.Title = Composition.GetView(title);
    sDialog.Show();
}

通常,模型不是作为某些方法调用的结果显示的,而是因为它们是 ItemsControl 中的一项或 ContentControl 的内容。在这种情况下,可以使用 XAML 中的 DataControl 控件通过调用 Composition.GetView() 来显示项。

注意:它还为 Silverlight 带来了 DataTemplate 类功能。

因为我们使用视图模型方法,所以同一个数据模型可以同时在多个地方显示,因此 Composition.GetView()DataViewAttributeDataControl 都有一个可选的 location 参数。

在下面的示例中,同一个 UserViewModel 实例(WorkspaceViewModel 的子类)用于使用不同的位置参数显示 TabItem 标题和内容(注意:第二个模板中未设置位置,即为 null)。

<Style x:Key="ClosableTabItemStyle" TargetType="TabItem" 
             BasedOn="{StaticResource {x:Type TabItem}}">
    <Setter Property="HeaderTemplate">
        <Setter.Value>
            <DataTemplate>
                <g:DataControl Data="{Binding}" Location="header"/>
            </DataTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <g:DataControl Data="{Binding}"/>
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

这两个视图的定义如下(注意:第一个视图是默认视图,即未设置位置,为 null

[DataView(typeof(CustomerViewModel))]
public partial class CustomerView : System.Windows.Controls.UserControl
{
    public CustomerView()
    {
        InitializeComponent();
    }
}
[DataView(typeof(WorkspaceViewModel), "header")]
public partial class CustomerHeaderView : UserControl
{
    public CustomerHeaderView()
    {
        InitializeComponent();
    }
}

验证和 ViewModelBase

Rob Eisenberg 的演讲的启发,我创建了一个 ViewModelBase 类,它实现了 WPF 开发的两个重要接口:INotifyPropertyChangedIDataErrorInfo

INotifyPropertyChanged 实现是强类型的(重构友好)

public class Person : ViewModelBase
{
    public string Name
    {
        get { return mName; }
        set
        {
            if (value == mName)
                return;
            mName = value;
            OnPropertyChanged(() => Name); // See, no magic string!
        }
    }
    string mName;
}

IDataErrorInfo 接口允许 WPF 绑定验证它们绑定的属性(如果 NotifyOnValidationError=true)。ViewModelBase 中的实现使用属性本身的 ValidationAttribute 来验证属性。例如

public class Person : ViewModelBase
{
    [Required]
    public string Name { get; set; }
 
    [Required]
    public string LastName { get; set; }
 
    [OpenRangeValidation(0, null)]
    public int Age { get; set; }
 
    [PropertyValidation("Name")]
    [PropertyValidation("LastName")]
    [DelegateValidation("InitialsError")]
    public string Initials { get; set; }
 
    public string InitialsError()
    {
        if (Initials == null || Initials.Length != 2)
            return "Initials is not a 2 letter string";
        return null;
    }
}

上面的示例还说明了这个库中提供的一些新的 ValidationAttribute 子类,位于 Galador.Applications.Validation 命名空间中,即

ConversionValidationAttribute
DelegateValidationAttribute
OpenRangeValidationAttribute
PropertyValidationAttribute

具有无效绑定的控件将自动用红色边框(默认样式)包围,但错误反馈可以像下面 XAML 片段中所示的那样进行自定义,该片段在验证文本下方显示错误消息

<!-- FIRST NAME-->
<Label 
    Grid.Row="2" Grid.Column="0" 
    Content="First _name:" 
    HorizontalAlignment="Right"
    Target="{Binding ElementName=firstNameTxt}"
    />
<TextBox 
    x:Name="firstNameTxt"
    Grid.Row="2" Grid.Column="2" 
    Text="{Binding Path=Customer.FirstName, ValidatesOnDataErrors=True, 
          UpdateSourceTrigger=PropertyChanged, BindingGroupName=CustomerGroup}" 
    Validation.ErrorTemplate="{x:Null}"
    />
<!-- Display the error string to the user -->
<ContentPresenter 
    Grid.Row="3" Grid.Column="2"
    Content="{Binding ElementName=firstNameTxt, Path=(Validation.Errors).CurrentItem}"
    />

断开消息传递

在复合应用程序中,组件需要相互发送消息而无需相互了解。Notifications 类及其静态方法用于解决此问题。

首先,应该在公共库中定义一个通用的消息类型;对象可以

  • 订阅此类型的消息(使用静态 Subscribe()Register() 方法)。
  • 发布消息(使用 Publish())。
  • 如果不再对消息感兴趣,则取消订阅(使用 Unsubscribe())。

注意:订阅线程是一个可选参数,可以是原始线程、UI 线程或后台线程。

为了说明这些功能,这是 Notifications 单元测试类中的一段代码。

public void TestSubscribe()
{
    // subscribe to a message
    Notifications.Subscribe<NotificationsTests, MessageData>(
                  null, StaticSubscribed, ThreadOption.PublisherThread);

    // publish a message
    Notifications.Publish(new MessageData { });

    // unsubscribe to a message below
    Notifications.Unsubscribe<NotificationsTests, MessageData>(null, StaticSubscribed);
}
 
static void StaticSubscribed(NotificationsTests t, MessageData md)
{
    // message handling
}

可以说,Notifications.Subscribe() 语法有点繁琐。因此,对象也可以通过调用 Notifications.Register(this) 一次订阅多个消息类型,该方法将订阅其所有带有单个参数且标记为 NotificationHandlerAttribute 的方法,如下所示

public void TestRegister()
{
    // register to multiple message type (1 shown below)
    Notifications.Register(this, ThreadOption.PublisherThread);

    // publish a message
    Notifications.Publish(new MessageData { Info = "h" });
}
 
[NotificationHandler]
public void Listen1(MessageData md)
{
    // message handling
}

命令

为了避免 UI 中出现代码,同时处理触发控件(如 ButtonMenuItem)的代码,WPF(以及 Silverlight 4)提供了命令(确切地说,是 ICommand)。当按钮被单击时,将触发控件操作,如果它有一个 Command 属性,它将调用 Command.Execute(parameter),其中 parameter 是 Control.CommandParameter 属性。

视图模型需要公开一个 Command 属性,其 Execute() 方法将调用其方法之一。为此,有一个 DelegateCommand

可以通过传递要执行的方法和可选的检查方法是否可以执行的方法(这将启用/禁用命令源,即按钮)来创建委托命令。例如

var p = new Person();
var save = new DelegateCommand(p, () => p.CanSave);

注意:第二个参数是一个表达式。该命令将自动 INotifyPropertyChanged 属性并注册到 PropertyChanged 事件以更新其 CanExecute() 状态。

注意:有时您需要像“DoAll”这样的命令,例如“CancelAll”或“BuyAll”,因此支持 ForeachCommand 类,它本身是一个 ICommand,并且可以监视 ICommand 列表,如果所有命令都可以执行,则将其状态设置为 CanBeExecuted

实用工具

一些其他非必要的特性也出现在这个库中。

UIThread Invoke

有一个 Invoker,它有助于在 GUI 线程上运行代码。它可以在 WPF 和 Silverlight 中使用。

public class Invoker
{
    public static void BeginInvoke(Delegate method, params object[] args)
    public static void DelayInvoke(TimeSpan delay, Delegate method, params object[] args)
}

设计时模型初始化

数据视图支持设计时。使用附加属性 Composition.DesignerDataContext 在数据视图上可以在设计时设置其 DataContext

<UserControl x:Class="MEFedMVVMDemo.Views.SelectedUser"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
        xmlns:g="http://schemas.galador.net/xaml/libraries"
        xmlns:models="clr-namespace:MEFedMVVMDemo.ViewModels"
        g:Composition.DesignerDataContext="models:SelectedUserViewModel"
        >

这些视图模型(即 DataViewDataContext)可以自行组合(即调用 Composition.Compose(this))以导入其他服务。

注意:拥有设计时 DataContext 使编写 DataTemplate 的体验变得更好。

注意:如果这些视图模型实现了 IDesignAware 接口,它们可以感知到它们处于设计模式。

注意:由模型加载的服务在运行时和设计时可能不同,如果它们使用 ExportService 而不是 Export 导出,如下所示

[ExportService(ServiceContext.DesignTime, typeof(IUsersService))]
public class DesignTimeUsersService : IUsersService

POCO 绑定

有时您想同步两个值。两个类有助于此过程:PropertyPathPOCOBinding。虽然这些类不是强类型的,但它们有一个强类型的 public static Create<T> 方法来帮助减轻创建它们的错误。以下是如何同步两个整数属性

var p1 = new Person();
var p2 = new Person();
POCOBinding.Create<Person, Person, int>(p1, aP => aP.Boss.Age, p2, aP2 => aP2.Age);

以下是如何通过 PropertyPath 设置值

var p1 = new Person();
var pp = PropertyPath.Create<string>(() => p1.Boss.Name);

//.....
pp.Value = "Foo";
Assert.AreEqual(p1.Boss.Name, "Foo");

注意PropertyPath 实现 INotifyPropertyChanged

Foreach

Foreach 类有多种变体,可用于观察 IEnumerableObservableCollection 并在集合中的某些内容发生更改时执行适当的操作。

总结

希望本文及其中包含的示例能展示复合应用程序架构的外观,以及此库如何轻松解决复合应用程序中最常遇到的关键问题

  • 使用 MEF 解析服务依赖关系。
  • 使用 DataControlComposition.GetView() 查找 DataView 以获取 DataModel。
  • 实现通用的 MVVM 模式:ICommand(使用 DelegateCommandForeachCommand)和断开消息传递(使用 Notifications)。
  • ViewModelBase 的子类中使用 ValidationAttribute 实现数据绑定验证。

此库的最新版本可以在 CodePlex 上找到。

兼容性

该库可与 .NET 4 和 Silverlight 4 的客户端配置文件一起使用。

如果需要移植到 .NET 3.5,存在两个障碍

  • MEF,位于 CodePlex
  • 以及 Validator 类,用于 ViewModelBase 中验证来自 ValidationAttribute 的属性,即实现 IDataErrorInfo 接口。只有两个方法需要从 Validator 中重新实现。

参考

© . All rights reserved.