复合应用程序重载






4.88/5 (38投票s)
一个更简单的复合应用程序库。
引言
在我开始一个新项目时,我正在寻找一个库来帮助我编写一个复合应用程序,即一个具有主 Shell(窗口)和可动态添加的可插入扩展(DLL/模块)的应用程序。一个像 Prism 这样的库,但希望更简单。
拼图的许多碎片已经可以在别处找到。应用程序必须在数据和视图之间有清晰的分离,即 MVVM 方法。服务必须与类似 MEF 的东西自动链接。数据验证应该是自动的(感谢 ValidationAttribute
)。
但是,在断开消息传递和视图解析方面需要改进。关于视图解析,即查找代表给定业务数据对象的合适视图的过程,我想用一个属性来标记视图,比如 DataView(typeof(BusinessData1))
,然后让库来处理其余的事情。这就是这个库的由来。
有什么新功能?
性能改进(在 Composition.GetView()
中)、命令简化、POCO 绑定、WP7 支持,以及 WP7 的 MEF。这是 CodePlex 项目。
目录
示例
为了测试我的库是否达到了目标,我将其移植了三个示例。在所有情况下,我都能够减小应用程序大小并保持功能。
- Josh Smith 的 MVVM 演示。这是最好的示例,因为它小巧简单,但(经过一些修改后)涵盖了库的几乎所有功能,并且是一个真正的复合应用程序。我能够摆脱手动编写的验证代码,而是使用
ValidationAttribute
。我调整了MainWindow
和App
类以使其成为复合应用程序,并在TabItem
中使用DataControl
来将多个控件绑定到具有不同视图的同一模型。 - Prism 的主示例,StockTraderApp 项目(庞大的示例)。我删除了 presenters(用于绑定视图和视图模型,现在已替换为对
Composition.GetView()
和DataControl
的调用)、EventAggregator
和自定义 prism 事件(被Notifications
静态方法替换)。最具挑战性和有趣的部分是摆脱RegionManager
并用IShellView
替换它,后者显式公开了可用的 Shell 区域,并摆脱了RegionManager
的魔术字符串方法。 - MEFedMVVM 库演示。该应用程序相对简单,但它广泛使用设计时支持,并且设计时体验非常出色。
单元测试说明了最重要的功能(即 Composition
、Notification
、ViewModelBase
和 Command
)是如何工作的。
关于 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 文档。要找到对象所需的所有导入的实现,需要执行两个操作
- 在程序开始时,应该从类型、程序集和/或目录的列表中初始化 MEF 的类型的“目录”,MEF 将在那里查找导出的位置。在这里,您可以选择感兴趣的模块。使用此库,您将调用方法
Composition.Register()
,如下所示。 - 您可以“组合”需要解析的对象(即包含导入的对象)。使用此库,您将使用方法
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,就可以编写复合应用程序。这涉及四个简短的步骤
- 定义要动态加载的 DLL
- 创建 Shell(或跳过并在 3 中导入)
- 导出 Shell 并导入模块
- 启动应用程序/初始化模块
例如,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
类。它还包含两个重要属性,Catalog
和 Container
,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()
、DataViewAttribute
和 DataControl
都有一个可选的 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 开发的两个重要接口:INotifyPropertyChanged
和 IDataErrorInfo
。
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 中出现代码,同时处理触发控件(如 Button
或 MenuItem
)的代码,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"
>
这些视图模型(即 DataView
的 DataContext
)可以自行组合(即调用 Composition.Compose(this)
)以导入其他服务。
注意:拥有设计时 DataContext
使编写 DataTemplate
的体验变得更好。
注意:如果这些视图模型实现了 IDesignAware
接口,它们可以感知到它们处于设计模式。
注意:由模型加载的服务在运行时和设计时可能不同,如果它们使用 ExportService
而不是 Export
导出,如下所示
[ExportService(ServiceContext.DesignTime, typeof(IUsersService))]
public class DesignTimeUsersService : IUsersService
POCO 绑定
有时您想同步两个值。两个类有助于此过程:PropertyPath
和 POCOBinding
。虽然这些类不是强类型的,但它们有一个强类型的 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
类有多种变体,可用于观察 IEnumerable
或 ObservableCollection
并在集合中的某些内容发生更改时执行适当的操作。
总结
希望本文及其中包含的示例能展示复合应用程序架构的外观,以及此库如何轻松解决复合应用程序中最常遇到的关键问题
- 使用 MEF 解析服务依赖关系。
- 使用
DataControl
或Composition.GetView()
查找 DataView 以获取 DataModel。 - 实现通用的 MVVM 模式:
ICommand
(使用DelegateCommand
和ForeachCommand
)和断开消息传递(使用Notifications
)。 - 在
ViewModelBase
的子类中使用ValidationAttribute
实现数据绑定验证。
此库的最新版本可以在 CodePlex 上找到。
兼容性
该库可与 .NET 4 和 Silverlight 4 的客户端配置文件一起使用。
如果需要移植到 .NET 3.5,存在两个障碍
- MEF,位于 CodePlex。
- 以及
Validator
类,用于ViewModelBase
中验证来自ValidationAttribute
的属性,即实现IDataErrorInfo
接口。只有两个方法需要从Validator
中重新实现。
参考
- CodePlex 上的 MEF(它也是 .NET 4 和 Silverlight 4 的一部分):http://mef.codeplex.com/。
- Prism,又名 Composite Application Library:http://compositewpf.codeplex.com/。
- Josh Smith 关于 MVVM:http://msdn.microsoft.com/en-us/magazine/dd419663.aspx。
- Rob Eisenberg 关于 MVVM:http://devlicio.us/blogs/rob_eisenberg/archive/2010/03/16/build-your-own-mvvm-framework-is-online.aspx。
- MEFedMVVM 库:http://mefedmvvm.codeplex.com。