附加的 VM 行为






4.91/5 (47投票s)
展示如何为 ViewModels 使用一种附加行为,以及如何构建大型 XAML 应用
引言
我有一段时间没写文章了,这篇文章在我待办事项列表里放了很久,所以我想是时候把它拿出来了。从某种意义上说,这篇文章很奇怪,因为它的一半内容有点“一次性”,仅仅是为了展示那些我认为非常有用并且可以应用于各种不同应用程序样式/平台的部分。
那么,这篇文章到底讲的是什么?
很简单,就是关于如何构建大型代码库。我并不是说没有其他方法,而是说这绝对是我个人认为非常有用的方法之一。为了让您了解我的当前代码库,我正在使用 MVVM/WPF,拥有大约 1500 个 ViewModels,仅 UI 代码库就大约有 150,000 行代码。
这不是我写的第一个 WPF/MVVM 应用程序,但当我将当前代码库与我以前工作的代码库进行比较时,我欣慰地知道我的新代码库更易于维护和测试。
这是如何实现的
是的,正如我在(我和其他人)的旧代码库中提到的,人们仍然在摸索 MVVM(当时是一个新模式,或者至少是一个重命名模式)以及 ViewModels 的组合等等。基本上,这是不熟悉的领域。好吧,有一些工具对此有所帮助,其中一个工具就是如今著名的 DelegateCommand
(或 Josh Smith 的 RelayCommand
),它允许您直接在 ViewModel 中运行 ICommand
实现代码。这很有帮助,很快每个人都在 ViewModel 中愉快地创建了大量的 DelegateCommand
。一些聪明的家伙甚至做得更多,正确地实现了 Command Pattern,但大多数人没有,只是在 ViewModel 中有大量的 DelegateCommand
。这没什么不对,直到……
您意识到 ViewModel 现在变得非常大,因为它必须公开的属性数量众多,并且还需要容纳大量的 DelegateCommand
。这些 DelegateCommand
中的每一个都可能调用其他服务,例如 WCF 代理、Http 服务等,并且 DelegateCommand
逻辑也可能处理重试和向用户显示错误,所以不难想象一个 DelegateCommand.Execute()
方法可能包含 30-100 行代码,将其乘以 ViewModel 中的 DelegateCommand
数量,您就可以轻松地看到事情如何失控。
很快我就意识到这不是一个好主意,我开始创建更模块化的 ViewModels。这在一段时间内有所帮助,但后来有时开始出现问题,我意识到在 ViewModel 之间的逻辑划分仍然存在一些情况,导致仍然存在一个大的 ViewModel,而且我找不到出路。我屈服了,偶尔还会叹息一下,好吧。
与此同时,我开始了一份新工作,在一家一流银行工作,遇到了两位非常聪明的开发人员,他们是
- Ray Booysen
- Keith Woods
这些人大量使用了 Reactive Extensions (RX),当时我对此还比较陌生,并且经常谈论 VM 行为。当时我并不完全理解 Ray/Keith 在说什么,但在我离开那份工作后,我仔细思考了这个问题,然后恍然大悟,我明白了。
Ray/Keith 所说的是创建孤立的功能的小片段,它们只做一件事(是的,我们老朋友的关注点分离 **SOC**),并将该功能片段与 ViewModel 实例关联起来。我喜欢这样称呼它:“ViewModels 的附加行为”。这是通过多种技术实现的,例如
- 子 IOC 容器
- RX
- 用于管理 ViewModel 的特定 IOC 服务
本文的其余部分将讲述我进入这个更光明世界的旅程(这句是开玩笑的)。这种技术可能不适合所有人,有些人甚至会认为它违反了“迪米特法则”,我个人认为应该将其重命名为“偶尔有用的迪米特法则”。
总之,我们开始文章吧。
演示视频
我发布了一个关于演示应用程序运行情况的小型 YouTube 视频。您可以通过点击下面的链接观看该视频
https://www.youtube.com/watch?v=d6rOiiXRZyY
屏幕截图
这里还有一张截图
点击图片查看更大版本
必备组件
演示代码是使用 VS2013 和 .NET 4.5 创建的,因此您需要同时拥有这两者才能正确运行演示应用程序。
代码在哪里?
本文的代码可以在我的 GitHub 账户上找到
https://github.com/sachabarber/WpfVMBehaviours
别忘了,如果您喜欢本文的其余部分以及其中的想法,您可以随时为 GitHub 存储库点星 ;-)
如何运行代码
下载代码后,您需要运行它。这很简单,只需确保在 Visual Studio IDE 中将以下项目设置为启动项目
WpfBehaviours.Shell
关于导航(骡子)的说明
我将这一部分命名为“骡子”,因为它实际上并不重要,它只是一个用于演示本文其余部分的载体。这并不是说骡子没有用,它们实际上非常有用了,而且可能有些人读完这篇文章后,会喜欢我打包“骡子(又名 PRISM 4.1)”的马鞍袋,并想亲自骑一骑。
对于这个演示应用程序,我仅将 PRISM 4.1 用于导航功能,因为我认为 PRISM 4.1 为 WPF/Silverlight/Windows Phone 应用程序提供了出色的导航框架。当然,我知道有一个更新的版本可用,但我有一些代码可以利用,这些代码针对的是 PRISM 4.1,而新版本的 PRISM 足够不同,我必须重做这项工作。正如我在本文中所说,这一切都关于其他事情,导航是一个附加的功能,我碰巧可以通过使用 PRISM 4.1 轻松提供。
事实上,您可以将我在这篇文章中讨论的技术应用于任何您可以使用的支持 IOC 容器和 RX 的 Windows 平台。您甚至可以将其应用于 Windows Forms 应用程序,在那里您可能会使用 Model-View-Presenter 模式。
我想我真正想说的是,有相当多的自定义 PRISM 4.1 区域代码是为了让 PRISM 4.1 能够使用附加演示文章中的子 IOC 容器进行导航,但您应该 不要太关注这一点,因为这并不是本文的真正重点
如果您对我是如何实现这一点感兴趣,可以阅读我以前写的一篇完全独立的文章
https://codeproject.org.cn/Articles/640573/ViewModel-st-Child-Container-PRISM-Navigation
好东西
从现在开始,是我认为本文中好的/有用的内容,可以应用于其他项目类型,例如
- WPF 应用程序
- Windows 应用商店应用程序
- Silverlight 应用程序
- Windows Phone 应用程序
- 通用应用程序
总体思路
这种架构的总体思路是,您可能提前知道您将面临一些非常棘手的 UI 要求。例如,我从事交易应用程序的创建工作,经常遇到涉及许多相互关联的 ViewModels 之间跨依赖关系的 UI 要求。其中可能与父 ViewModels,甚至祖父母 ViewModels,或兄弟 ViewModels 存在关系。这可能是一场噩梦,想象一下所有那些您不仅需要监控,而且还需要在正确的时间取消订阅的 INotifyPropertyChanged
事件。唉!
现在,试图将所有这些塞进一个 ViewModel 是行不通的。关键是我们生活在现代,可以使用现代工具,对我来说,这些工具包括 RX/IOC 容器。它们无疑是实现我在此呈现的演示应用程序/框架的两个重要组成部分。
如果所有这些听起来都很模糊,不用担心,您会看到很多代码,我们只是在为它铺垫。
那么基本思路是什么呢?在我们开始之前,我们需要稍微更深入地思考一下我们的 UI。就像我说的那样,我从事交易应用程序的创建工作,在那里我通常需要显示可关闭的、相同类型 ViewModel 的选项卡/磁贴(当然是不同的实例)。所以,将该 ViewModel 及其依赖项的创建与创建同一类型 ViewModel 的另一个实例隔离开来是有意义的。
现在,您可能已经遇到过很多关于如何将服务注入 ViewModel 实例的控制反转(IOC)示例。问题是,您从未见过太多真正处理 IOC 组件生命周期以及它如何在您的应用程序中工作的示例。
- 想象一下您有一个共享服务,但您希望它仅对特定类型的 ViewModel 保持单例。嗯。
- IOC 容器何时知道释放事物?嗯。
- 或者您希望每个磁贴都获得一个仅作用于自身的单例实例。嗯。
我对此思考了很多,IOC 解析的依赖关系和高度可组合的 UI 是我感兴趣/关心的事情。这就是本文真正要讲的内容。
所以,一些要点
- 我们将假设对于每个要导航到的新部分(视图),我们将使用一个子 IOC 容器
- 我们将(强)耦合新创建的 IOC 容器与 ViewModel 的生命周期,以便当 ViewModel 被关闭(不再需要)时,子容器及其为当前 ViewModel 创建的所有已保存依赖项都将被处置
- 我们将创建一组 ViewModel 行为(有些人可能见过 MVVM + Controller,所以您可以将 ViewModel 行为视为 ViewModel 实例的微型控制器)。这些“微型控制器”的美妙之处在于,您可以非常方便地找到执行特定操作的代码在哪里,并且您会知道它不会影响其他任何东西,因为它也遵循关注点分离的理想。
- ViewModel 行为将仅通过一个 IOC 注册来应用
- ViewModel 行为将能够使用 RX 监控 ViewModel(这太棒了,我越用越喜欢它)
有些人可能会因为这种“过于理想化”而退缩,是的,这是公平的,但老实说,通过遵循这种模式,我感觉我终于找到了我的 XAML 极乐/甜蜜点。
总之,这里有一张图,希望能说明我在这里所说的事情
这里还有一张截图
点击图片查看更大版本
深入了解其内部细节
这一部分将更深入地探讨如何实现上述所有优点。正如我已经说过的,我正在使用 PRISM 4.1,但正如我也说过的,本文已经/将要演示的技术同样适用于任何具有有状态 UI 和导航的地方,例如 Windows Phone 应用程序、Silverlight,甚至 Windows Forms 也可以是这方面的不错选择(在那里您可能会使用 Model-View-Presenter 模式而不是 MVVM)。
用组合视图的思路思考
正如我所说,这种模式(如果您愿意这么称呼的话)对于可以显示可重复信息片段的 UI(例如选项卡、带工作区的磁贴等)非常有意义。每个选项卡或磁贴可能都有自己的子 IOC 容器。
这是演示应用程序的屏幕截图,它使用了磁贴,每个磁贴都有自己的子 IOC 容器,并且每个磁贴都有自己的 VM 行为集,这些行为与 VM 和子容器相关联。子 IOC 容器被添加到磁贴 VM,以便当 VM 的 DataTemplate
上的关闭按钮被点击时,它会调用 VM 中的一个 ICommand,该命令将处置 VM 的 disposables,其中包括子 IOC 容器。
在我看来,一切都干净整洁,自成一体。
点击图片查看更大版本
好了,关于它是如何工作的就聊到这里,你们想看代码,对吧?从现在开始,一切都围绕着代码。
可处置的 ViewModels / 子容器生命周期管理
正如我在文章中多次提到的,我们将子 IOC 容器作为 IDisposable
耦合到 ViewModel,以便在 ViewModel 被要求关闭时(通常通过关闭 ICommand
实现),它可以调用所有 ViewModel 持有的 IDisposable
的 Dispose()
,其中包括子 IOC 容器。
实现这一点使用了两样东西。
可处置的 ViewModel
首先,我们需要一个特殊的基类来为我们的 ViewModels(只有需要子容器的顶层 ViewModels 才需要继承这个新基类)。我个人发现能够向 VM 添加 IDisposable
实例非常有用)。
public abstract class DisposableViewModel : INPCBase, IDisposable
{
private CompositeDisposable disposables = new CompositeDisposable();
public void AddDisposable(IDisposable disposable)
{
disposables.Add(disposable);
}
public virtual void Dispose()
{
disposables.Dispose();
}
}
在这里可以看到有一个 void AddDisposable(IDisposable disposable)
方法,它接受 IDisposable
实例并将其添加到 CompositeDisposable
(Rx 类,它基本上就是一个 List<IDisposable>
)。此演示应用程序中的其他 ViewModel(s) 将继承如下(在此代码中,还有一个基类 TileViewModelBase
,它实际上继承自 DisposableViewModel
,但希望您能理解)。
public class SpotTileViewModel : TileViewModelBase, INavigationAware
{
private readonly IViewModelController controller;
public SpotTileViewModel(
Func<SpotTileViewModel, IViewModelController> controllerFactory,
IRegionManager regionManager,
IMessageBoxService messageBoxService)
: base(regionManager, messageBoxService)
{
controller = controllerFactory(this);
this.AddDisposable(controller);
controller.Start();
}
}
将子 IOC 容器添加到 ViewModel 作为 IDisposable
另一部分是,当我们响应导航请求时(正如我所说,我正在使用 PRISM 4.1,但这也可以使用您现有的任何导航过程来完成),我们将为当前导航请求配置一个子 IOC 容器,然后将该子容器添加到 DisposableViewModel
(正在导航到的那个)。
由于我使用的是 PRISM 4.1,我使用的是 Microsoft Unity IOC 容器,但您可以在这里使用您选择的另一个容器,只要它支持创建子容器。
private void NavigateToNewSpotTile(ShowNewSpotTileMessage showNewSpotTileMessage)
{
if (regionNavigationCapacityChecker.IsNavigationAllowedForRegion(RegionNames.MainRegion))
{
UriQuery parameters = new UriQuery();
parameters.Add("UniqueId", Guid.NewGuid().ToString());
IUnityContainer childContainer = ConfigureSpotTileContainer();
var uri = new Uri(typeof(SpotTileViewModel).FullName + parameters, UriKind.RelativeOrAbsolute);
regionManager.RequestNavigateUsingSpecificContainer(RegionNames.MainRegion, uri,
regionNavigationCallbackHelper.HandleNavigationCallback, childContainer);
}
}
private IUnityContainer ConfigureSpotTileContainer()
{
IUnityContainer childContainer = container.CreateChildContainer();
//navigation windows
childContainer.RegisterTypeForNavigationWithChildContainer<SpotTileViewModel>(
new HierarchicalLifetimeManager());
//viwemodel controllers
childContainer.RegisterType<IViewModelController, SpotTileViewModelController>(
"SpotTileViewModelController", new HierarchicalLifetimeManager());
//viewmodel controller factories
childContainer.RegisterType<Func<SpotTileViewModel, IViewModelController>>(
new HierarchicalLifetimeManager(),
new InjectionFactory(c =>
new Func<SpotTileViewModel, IViewModelController>(
viewModel =>
c.Resolve<IViewModelController>("SpotTileViewModelController",
new DependencyOverride<SpotTileViewModel>(viewModel)))));
//behaviours
childContainer.RegisterType<ISpotTileViewModelBehaviour,
LoadFakeSpotCCYPairsBehaviour>("LoadFakeSpotCCYPairsBehaviour",
new HierarchicalLifetimeManager());
childContainer.RegisterType<ISpotTileViewModelBehaviour,
MonitorFakePairBehaviour>("MonitorFakePairBehaviour",
new HierarchicalLifetimeManager());
childContainer.RegisterType<ISpotTileViewModelBehaviour,
OkCommandBehaviour>("OkCommandBehaviour",
new HierarchicalLifetimeManager());
childContainer.RegisterType<ISpotTileViewModelBehaviour,
TimeoutBehaviour>("TimeoutBehaviour",
new HierarchicalLifetimeManager());
//services
childContainer.RegisterType<IFakeSpotRateProvider,
FakeSpotRateProvider>(new HierarchicalLifetimeManager());
return childContainer;
}
在上述代码中需要注意的重要一点可以归结为以下几点
- 我们创建一个子容器
- 我们使用 Unity HierarchicalLifetimeManager 在子容器中注册服务,这意味着它们是子容器作用域内的单例实例
那么子容器添加到 DisposableViewModel 的地方在哪里?
对我来说,这是在下面的扩展方法中完成的
public static void RegisterTypeForNavigationWithChildContainer<T>(this IUnityContainer container, LifetimeManager lifetimeManager)
{
container.RegisterType(typeof(Object), typeof(T), typeof(T).FullName,
lifetimeManager,
new InjectionMethod("AddDisposable", new object[] { container }));
}
但正如我所说,您可能需要根据您的需求找到自己的方法来做到这一点,唯一重要的一点是您将子容器添加到正在导航到的 DisposableViewModel
。
ViewModel 导航 / 子容器
所以我们现在知道了一些正在发生的事情,我们知道当请求导航到一个 ViewModel 时,我们会创建一个新的子容器,并且我们将子容器添加到这个 ViewModel。
那么我们如何处理显示 ViewModel 呢?对我来说,答案是使用一个代表 ViewModel 的 DataTemplate
。这是演示应用程序的 SpotTileViewModel
的 DataTemplate
。
<DataTemplate DataType="{x:Type viewModels:SpotTileViewModel}">
<Border BorderBrush="{DynamicResource AccentBrush}" BorderThickness="2,2,2,2"
Background="White" IsHitTestVisible="True">
<Grid Background="White"
attachedProps:GridUtils.ColumnDefinitions="*"
attachedProps:GridUtils.RowDefinitions="Auto,Auto,Auto,*,Auto">
<Grid HorizontalAlignment="Stretch"
Background="{DynamicResource AccentBrush}">
<Label Foreground="White" Content="Spot Tile"
VerticalAlignment="Center"
VerticalContentAlignment="Center" Margin="2,0,0,0" />
<Button Style="{DynamicResource toolbarButtonStyle}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
VerticalContentAlignment="Center" Margin="0,0,0,0"
Command="{Binding CloseViewCommand}"
ToolTip="Close">
<Viewbox Width="40" Height="40">
<Grid>
<Grid Width="128" Height="128" >
<Rectangle Fill="{Binding
RelativeSource={RelativeSource
AncestorType={x:Type Button} }, Path=Background}"
Margin="20"/>
</Grid>
<Path Data="F1M54.0573,47.8776L38.1771,31.9974 54.0547,16.1198C55.7604,14.4141 55.7604,11.6511 54.0573,9.94531 52.3516,8.23962 49.5859,8.23962 47.8802,9.94531L32.0026,25.8229 16.1224,9.94531C14.4167,8.23962 11.6511,8.23962 9.94794,9.94531 8.24219,11.6511 8.24219,14.4141 9.94794,16.1198L25.8255,32 9.94794,47.8776C8.24219,49.5834 8.24219,52.3477 9.94794,54.0534 11.6511,55.7572 14.4167,55.7585 16.1224,54.0534L32.0026,38.1745 47.8802,54.0534C49.5859,55.7585 52.3516,55.7572 54.0573,54.0534 55.7604,52.3477 55.763,49.5834 54.0573,47.8776z"
Stretch="Uniform"
Fill="{Binding RelativeSource={RelativeSource AncestorType={x:Type Button} },
Path=Foreground}" Width="26" Height="26" />
</Grid>
</Viewbox>
</Button>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<ComboBox HorizontalAlignment="Center" Width="80" Margin="2"
ItemsSource="{Binding FakeSpotPairs}"
SelectedItem="{Binding FakeSpotPair}"
IsEnabled="{Binding IsEnabled}"/>
<DatePicker SelectedDate="{Binding SelectedDate}"
Width="100" Margin="2"
IsEnabled="{Binding IsEnabled}"/>
</StackPanel>
<Grid Background="White" Grid.Row="2"
attachedProps:GridUtils.ColumnDefinitions="*,*"
attachedProps:GridUtils.RowDefinitions="Auto">
<ContentControl Content="{Binding RateViewModel}"
Margin="2,0,0,0"
IsEnabled="{Binding IsEnabled}"/>
<StackPanel Grid.Row="0" Grid.Column="1"
Orientation="Vertical"
Visibility="{Binding StartedTiming,
Converter={x:Static
converters:BoolToVisibilityConverter.Instance},
ConverterParameter='True'}">
<ProgressBar Value="{Binding Progress}"
Style="{StaticResource SegmentedProgressBarStyle}"
Margin="5" Width="40" Height="40"></ProgressBar>
<Label Content="{Binding TimeOutRemaining}"
IsEnabled="{Binding IsEnabled}"
VerticalAlignment="Bottom"
HorizontalAlignment="Center" Margin="2"/>
</StackPanel>
</Grid>
<Button Content="Ok" Margin="2" Grid.Row="4"
Command="{Binding OkCommand}"/>
</Grid>
</Border>
</DataTemplate>
清理导航请求的 IOC 服务
那么我们如何 Dispose()
子容器及其所有持有的资源呢?嗯,理想情况下,DataTemplate/ViewModel 对将有一个关闭按钮(或其他移除方式)来将 ViewModel 从工作区中移除。在演示应用程序中,当关闭按钮被点击时,它将运行以下代码,该代码处置 ViewModel 持有的所有 IDisposable
实例。子 IOC 容器是其中之一。
简单来说,当 ViewModel 被移除时,子容器及其持有的所有服务都会被要求 Dispose()
。
public abstract class TileViewModelBase : DisposableViewModel
{
public TileViewModelBase(
IRegionManager regionManager,
IMessageBoxService messageBoxService)
{
this.regionManager = regionManager;
this.messageBoxService = messageBoxService;
CloseViewCommand = new SimpleCommand<object, object>(ExecuteCloseViewCommand);
}
public ICommand CloseViewCommand { get; set; }
private void ExecuteCloseViewCommand(Object arg)
{
var result = messageBoxService.ShowYesNo(
"You are about to close this Option, you will loose any edits you have. Are you sure?",
"Confirm close",
CustomDialogIcons.Warning);
if (result == CustomDialogResults.Yes)
{
IRegion region = regionManager.Regions["MainRegion"];
region.Remove(this);
this.Dispose();
}
}
}
ViewModel 控制器与“ViewModel 行为”案例
使这一切正常工作的另一件事是将大部分繁琐的 ICommand 逻辑(和其他逻辑)移出 ViewModel,以便 ViewModel 可以专注于做它需要做的事情,即成为视图的模型。那么我们如何将东西移出 ViewModel 呢?
诀窍在于演示应用程序的两个方面。
ViewModel/控制器关系
对于导航到的每个顶级 VM(使用您喜欢的任何技术),VM 和单个 Controller 之间都有一个 1:1 的映射。Controller 以 VM 作为依赖项,以及一组 VM 行为(我们稍后会看到)。
- ViewModel 的作用是接受一个 Controller 的工厂作为依赖项,然后使用该工厂,将自身传递给 Controller 工厂,该工厂返回一个 VM 的 Controller。然后 VM 调用 Start(我非常喜欢确定性地启动事物)。VM 将 Controller 添加为 VM 的
IDisposable
列表中的IDisposable
,以便在 VM 被处置时,Controller 及其依赖项也能被清理。 - Controller 将 VM 和一组特定的 VM 行为作为构造函数参数。当 Controller 上的 Start 方法被调用时,它将依次调用每个特定 VM 行为的 Start()。Controller 还会将每个特定 VM 行为添加到它自己的
IDisposable
列表中。
这是实现这一目标的relevant 代码
VM 代码
public class SpotTileViewModel : TileViewModelBase, INavigationAware
{
private readonly IViewModelController controller;
public SpotTileViewModel(
Func<SpotTileViewModel, IViewModelController> controllerFactory,
....)
: base(.....)
{
controller = controllerFactory(this);
this.AddDisposable(controller);
controller.Start();
}
}
这里的重点是 VM 获取一个工厂来创建 Controller,然后使用它
Controller 代码
public class SpotTileViewModelController : IViewModelController
{
private CompositeDisposable disposables = new CompositeDisposable();
private readonly SpotTileViewModel spotTileViewModel;
private readonly ISpotTileViewModelBehaviour[] behaviors;
public SpotTileViewModelController(
SpotTileViewModel spotTileViewModel,
ISpotTileViewModelBehaviour[] behaviors)
{
this.spotTileViewModel = spotTileViewModel;
this.behaviors = behaviors;
}
public void Start()
{
//start behaviors
foreach (var behavior in behaviors)
{
disposables.Add(behavior);
behavior.Start(spotTileViewModel);
}
}
public void Dispose()
{
disposables.Dispose();
}
}
这次的重点是 Controller 期望特定类型的 VM 以及一组特定类型的 VM 行为。然后它调用这些特定 VM 行为中的每一个的 Start()
方法,并将实际的 VM 传递进去
IOC Wireup 代码
显然,有一些“魔法”可以很好地将所有这些连接起来。这是 IOC 容器的工作。如下所示。正如我之前所述,我使用的是 Microsoft Unity 应用程序块,所以您可能需要根据您的需求/选择的 IOC 容器进行更改。
private IUnityContainer ConfigureSpotTileContainer()
{
IUnityContainer childContainer = container.CreateChildContainer();
//navigation windows
childContainer.RegisterTypeForNavigationWithChildContainer<SpotTileViewModel>(
new HierarchicalLifetimeManager());
//viwemodel controllers
childContainer.RegisterType<IViewModelController, SpotTileViewModelController>(
"SpotTileViewModelController", new HierarchicalLifetimeManager());
//viewmodel controller factories
childContainer.RegisterType<Func<SpotTileViewModel, IViewModelController>>(
new HierarchicalLifetimeManager(),
new InjectionFactory(c =>
new Func<SpotTileViewModel, IViewModelController>(
viewModel =>
c.Resolve<IViewModelController>("SpotTileViewModelController",
new DependencyOverride<SpotTileViewModel>(viewModel)))));
//behaviours
childContainer.RegisterType<ISpotTileViewModelBehaviour,
LoadFakeSpotCCYPairsBehaviour>("LoadFakeSpotCCYPairsBehaviour",
new HierarchicalLifetimeManager());
childContainer.RegisterType<ISpotTileViewModelBehaviour,
MonitorFakePairBehaviour>("MonitorFakePairBehaviour",
new HierarchicalLifetimeManager());
childContainer.RegisterType<ISpotTileViewModelBehaviour,
OkCommandBehaviour>("OkCommandBehaviour",
new HierarchicalLifetimeManager());
childContainer.RegisterType<ISpotTileViewModelBehaviour,
TimeoutBehaviour>("TimeoutBehaviour",
new HierarchicalLifetimeManager());
//services
childContainer.RegisterType<IFakeSpotRateProvider,
FakeSpotRateProvider>(new HierarchicalLifetimeManager());
return childContainer;
}
ViewModel 行为
啊,终于到了文章的核心部分了。那么这些 VM 行为是什么呢?我喜欢将它们视为附加 VM 行为或微型控制器,其中每个行为都为相关的 ViewModel 完成一个特定的工作。
那么我们如何创建一个 VM 行为呢?嗯,这始于一个特定于您 VM 需求的接口。这是用于演示应用程序的接口
public interface ISpotTileViewModelBehaviour : IDisposable
{
void Start(SpotTileViewModel spotTileViewModel);
}
请记住,这些是由相关 VM 的一个 Controller 使用和启动的。其思想是每个行为只做一件事,并且只做一件事。这可能包括
- 监听某个属性的变化(例如 ID,这会导致整个实体从数据库中获取并填充)
- 监听一组相关的属性,这些属性必须全部设置完毕才能执行某些操作
- 监听命令的执行(我们将为此使用 RX,但稍后会详细介绍)
通过遵循这种设计,可以非常轻松地隔离事物,查找事物,并且可以相当确定您正在修改的代码不会影响系统的其他部分。关注点分离等等。
例如,考虑演示应用程序的这个屏幕截图
如果您想找出 OkCommand 实现有什么问题,您会在哪里查找,哦,在“OkCommandBehavior”中,您说。
如果您想找出超时实现有什么问题,您会在哪里查找,哦,在“TimeOutBehavior”中,您说。
等等……您明白了。
注意:一些读者可能会简单地认为这是“正确”/“糟糕”(取决于您的看法/争论方式)的“Command Pattern”实现,但它要灵活得多。您不一定需要执行基于用户输入的动作,如上所述,它可以基于其他 VM 属性的变化。您有 VM 的范围,因此它或其任何子属性/VM 也可用于更改/监听。
响应式命令
这种模式的另一个非常有用的地方是使用一个 ReactiveCommand
,它内部使用 RX。ReactiveCommand 仍然实现 ICommand
,并在命令执行时 OnNext()
一个内部 RX Subject。通过使用 ReactiveCommand
,这意味着您可以在任何可以看到 ViewModel 的地方使用 RX 操作符的全部功能,并且有大量的操作符(与 LINQ 一样多,甚至更多),并且能够在命令执行时订阅它。
这是 ReactiveCommand
的实现
public class ReactiveCommand<T1, T2> : ICommand, IReactiveCommand
{
private Func<T1, bool> canExecuteMethod;
private Subject<object> commandExecutedSubject = new Subject<object>();
public ReactiveCommand()
{
this.canExecuteMethod = (x) => { return true; };
}
public ReactiveCommand(Func<T1, bool> canExecuteMethod)
{
this.canExecuteMethod = canExecuteMethod;
}
public bool CanExecute(T1 parameter)
{
if (canExecuteMethod == null) return true;
return canExecuteMethod(parameter);
}
public void Execute(T2 parameter)
{
commandExecutedSubject.OnNext(parameter);
}
public bool CanExecute(object parameter)
{
return CanExecute((T1)parameter);
}
public void Execute(object parameter)
{
Execute((T2)parameter);
}
public event EventHandler CanExecuteChanged
{
add
{
if (canExecuteMethod != null)
{
CommandManager.RequerySuggested += value;
}
}
remove
{
if (canExecuteMethod != null)
{
CommandManager.RequerySuggested -= value;
}
}
}
public IObservable<object> CommandExecutedStream
{
get { return this.commandExecutedSubject.AsObservable(); }
}
}
可以看到 ReactiveCommand
还接受一个 CanExecute
委托,这意味着您可以直接在您的 ViewModel 中使用它。执行内部的另一个重要部分是调用 Subject<object>
的 OnNext()
,其中 object 是 ICommand
参数,您可以从代码或 XAML 提供。
这是一个来自 ViewModel 的示例用法
OkCommand = new ReactiveCommand<object, object>(x => IsValid() && IsEnabled);
这是在命令执行一次后订阅它的示例。我包含了一个完整的 VM 行为,以便您了解使用 ReactiveCommand
和 VM 行为的理念(每个只做一件事)。
public class OkCommandBehaviour : ISpotTileViewModelBehaviour
{
private readonly IEventMessager eventMessager;
private SpotTileViewModel spotTileViewModel;
private CompositeDisposable disposables = new CompositeDisposable();
public OkCommandBehaviour(IEventMessager eventMessager)
{
this.eventMessager = eventMessager;
}
public void Dispose()
{
disposables.Dispose();
}
public void Start(SpotTileViewModel spotTileViewModel)
{
this.spotTileViewModel = spotTileViewModel;
SetupTopLevelSubscription();
}
private void SetupTopLevelSubscription()
{
disposables.Add(spotTileViewModel.OkCommand.CommandExecutedStream.Subscribe(
x =>
{
spotTileViewModel.IsEnabled = false;
eventMessager.Publish(new SpotTrade(
spotTileViewModel.SelectedDate,
spotTileViewModel.FakeSpotPair,
spotTileViewModel.RateViewModel.WholeRate,
DateTime.Now));
}));
}
}
进一步支持 RX
RX 的另一个用例是监视 XAML ViewModel 通常实现的 INotifyPropertyChanged
接口。或者甚至监听 ObservableCollection
的变化,其中项目被添加/删除。我通常会准备很多扩展方法来帮助处理这个问题。这是演示项目中的一个
public class ItemPropertyChangedEvent<TSender>
{
public TSender Sender { get; set; }
public PropertyInfo Property { get; set; }
public bool HasOld { get; set; }
public object OldValue { get; set; }
public object NewValue { get; set; }
public override string ToString()
{
return string.Format("Sender: {0}, Property: {1}, HasOld: {2}, OldValue: {3}, NewValue: {4}", this.Sender, this.Property, this.HasOld, this.OldValue, this.NewValue);
}
}
public class ItemPropertyChangedEvent<TSender, TProperty>
{
public TSender Sender { get; set; }
public PropertyInfo Property { get; set; }
public bool HasOld { get; set; }
public TProperty OldValue { get; set; }
public TProperty NewValue { get; set; }
}
public class ItemChanged<T>
{
public T Item { get; set; }
public bool Added { get; set; }
public NotifyCollectionChangedEventArgs EventArgs { get; set; }
}
public class WeakSubscription<T> : IDisposable, IObserver<T>
{
private readonly WeakReference reference;
private readonly IDisposable subscription;
private bool disposed;
public WeakSubscription(IObservable<T> observable, IObserver<T> observer)
{
this.reference = new WeakReference(observer);
this.subscription = observable.Subscribe(this);
}
void IObserver<T>.OnCompleted()
{
var observer = (IObserver<T>)this.reference.Target;
if (observer != null)
observer.OnCompleted();
else
this.Dispose();
}
void IObserver<T>.OnError(Exception error)
{
var observer = (IObserver<T>)this.reference.Target;
if (observer != null)
observer.OnError(error);
else
this.Dispose();
}
void IObserver<T>.OnNext(T value)
{
var observer = (IObserver<T>)this.reference.Target;
if (observer != null)
observer.OnNext(value);
else
this.Dispose();
}
public void Dispose()
{
if (!this.disposed)
{
this.disposed = true;
this.subscription.Dispose();
}
}
}
public static class ObservableExtensions
{
public static IObservable<Unit> AsUnit<TValue>(this IObservable<TValue> source)
{
return source.Select(x => new Unit());
}
public static IObservable<TItem> ObserveWeakly<TItem>(this IObservable<TItem> source)
{
return Observable.Create<TItem>(obs =>
{
var weakSubscription = new WeakSubscription<TItem>(source, obs);
return () =>
{
weakSubscription.Dispose();
};
});
}
public static IObservable<Unit> ObserveCollectonChanged<T>(this T source)
where T : INotifyCollectionChanged
{
var observable = Observable
.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
h => source.CollectionChanged += h,
h => source.CollectionChanged -= h)
.AsUnit();
return observable;
}
public static IObservable<Unit> ObserveCollectonChanged<T>(this T source, NotifyCollectionChangedAction collectionChangeAction)
where T : INotifyCollectionChanged
{
var observable = Observable
.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
h => source.CollectionChanged += h,
h => source.CollectionChanged -= h)
.Where(x => x.Action == collectionChangeAction)
.AsUnit();
return observable;
}
public static IObservable<ItemChanged<T>> ItemChanged<T>(this ObservableCollection<T> collection, bool fireForExisting = false)
{
var observable = Observable.Create<ItemChanged<T>>(obs =>
{
NotifyCollectionChangedEventHandler handler = null;
handler = (s, a) =>
{
if (a.NewItems != null)
{
foreach (var item in a.NewItems.OfType<T>())
{
obs.OnNext(new ItemChanged<T>()
{
Item = item,
Added = true,
EventArgs = a
});
}
}
if (a.OldItems != null)
{
foreach (var item in a.OldItems.OfType<T>())
{
obs.OnNext(new ItemChanged<T>()
{
Item = item,
Added = false,
EventArgs = a
});
}
}
};
collection.CollectionChanged += handler;
return () =>
{
collection.CollectionChanged -= handler;
};
});
if (fireForExisting)
observable = observable.StartWith(Scheduler.CurrentThread, collection.Select(i => new ItemChanged<T>()
{
Item = i,
Added = true,
EventArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, i)
}).ToArray());
return observable;
}
public static IObservable<TObserved> ObserveInner<TItem, TObserved>(this ObservableCollection<TItem> collection, Func<TItem, IObservable<TObserved>> observe)
{
return Observable.Create<TObserved>(obs =>
{
Dictionary<TItem, IDisposable> subscriptions = new Dictionary<TItem, IDisposable>();
var mainSubscription =
collection.ItemChanged(true)
.Subscribe(change =>
{
IDisposable subscription = null;
subscriptions.TryGetValue(change.Item, out subscription);
if (change.Added)
{
if (subscription == null)
{
subscription = observe(change.Item).Subscribe(obs);
subscriptions.Add(change.Item, subscription);
}
}
else
{
if (subscription != null)
{
subscriptions.Remove(change.Item);
subscription.Dispose();
}
}
});
return () =>
{
mainSubscription.Dispose();
foreach (var subscription in subscriptions)
subscription.Value.Dispose();
};
});
}
public static IObservable<TValue> ObserveProperty<T, TValue>(this T source,
Expression<Func<T, TValue>> propertyExpression) where T : INotifyPropertyChanged
{
return source.ObserveProperty(propertyExpression, false);
}
public static IObservable<TValue> ObserveProperty<T, TValue>(this T source,
Expression<Func<T, TValue>> propertyExpression,
bool observeInitialValue) where T : INotifyPropertyChanged
{
var getter = propertyExpression.Compile();
var observable = Observable
.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
h => source.PropertyChanged += h,
h => source.PropertyChanged -= h)
.Where(x => x.PropertyName == propertyExpression.GetPropertyName())
.Select(_ => getter(source));
if (observeInitialValue)
return observable.Merge(Observable.Return(getter(source)));
return observable;
}
public static IObservable<string> ObservePropertyChanged<T>(this T source)
where T : INotifyPropertyChanged
{
var observable = Observable
.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
h => source.PropertyChanged += h,
h => source.PropertyChanged -= h)
.Select(x => x.PropertyName);
return observable;
}
public static IObservable<ItemPropertyChangedEvent<TItem, TProperty>> ObservePropertyChanged<TItem, TProperty>(this TItem target, Expression<Func<TItem, TProperty>> propertyName, bool fireCurrentValue = false) where TItem : INotifyPropertyChanged
{
var property = ExpressionExtensions.GetPropertyName(propertyName);
return ObservePropertyChanged(target, property, fireCurrentValue)
.Select(i => new ItemPropertyChangedEvent<TItem, TProperty>()
{
HasOld = i.HasOld,
NewValue = (TProperty)i.NewValue,
OldValue = i.OldValue == null ? default(TProperty) : (TProperty)i.OldValue,
Property = i.Property,
Sender = i.Sender
});
}
public static IObservable<ItemPropertyChangedEvent<TItem>> ObservePropertyChanged<TItem>(this TItem target, string propertyName = null, bool fireCurrentValue = false) where TItem : INotifyPropertyChanged
{
if (propertyName == null &&& fireCurrentValue)
throw new InvalidOperationException("You need to specify a propertyName if you want to fire the current value of your property");
return Observable.Create<ItemPropertyChangedEvent<TItem>>(obs =>
{
Dictionary<PropertyInfo, object> oldValues = new Dictionary<PropertyInfo, object>();
Dictionary<string, PropertyInfo> properties = new Dictionary<string, PropertyInfo>();
PropertyChangedEventHandler handler = null;
handler = (s, a) =>
{
if (propertyName == null || propertyName == a.PropertyName)
{
PropertyInfo prop = null;
if (!properties.TryGetValue(a.PropertyName, out prop))
{
prop = typeof(TItem).GetProperty(a.PropertyName);
properties.Add(a.PropertyName, prop);
}
var change = new ItemPropertyChangedEvent<TItem>()
{
Sender = target,
Property = prop,
NewValue = prop.GetValue(target, null)
};
object oldValue = null;
if (oldValues.TryGetValue(prop, out oldValue))
{
change.HasOld = true;
change.OldValue = oldValue;
oldValues[prop] = change.NewValue;
}
else
{
oldValues.Add(prop, change.NewValue);
}
obs.OnNext(change);
}
};
target.PropertyChanged += handler;
if (propertyName != null && fireCurrentValue)
handler(target, new PropertyChangedEventArgs(propertyName));
return () =>
{
target.PropertyChanged -= handler;
};
});
}
}
在这里,您会找到许多用于处理 RX 的有用方法,它在很多场合都拯救了我。
典型的 VM 行为代码
我认为结束这篇文章的一个好方法是列出演示应用程序中的一些行为,以便您能了解它们。您上面已经看到过 OkBehaviour
,所以让我们看看几个不同的。
MonitorFakePairBehaviour
这个行为的作用是监听选择的货币对,然后监听来自假费率对符号 tick 器的变化
public class MonitorFakePairBehaviour : ISpotTileViewModelBehaviour
{
private readonly IFakeSpotRateProvider fakeSpotRateProvider;
private SpotTileViewModel spotTileViewModel;
private CompositeDisposable disposables = new CompositeDisposable();
private CompositeDisposable fakePairDisposables = new CompositeDisposable();
public MonitorFakePairBehaviour(IFakeSpotRateProvider fakeSpotRateProvider)
{
this.fakeSpotRateProvider = fakeSpotRateProvider;
disposables.Add(this.fakeSpotRateProvider);
}
public void Dispose()
{
disposables.Dispose();
fakePairDisposables.Dispose();
}
public void Start(SpotTileViewModel spotTileViewModel)
{
this.spotTileViewModel = spotTileViewModel;
SetupTopLevelSubscription();
}
private void SetupTopLevelSubscription()
{
//listen for changes in the number of legs
DisposableHelper.CreateNewCompositeDisposable(ref fakePairDisposables);
fakePairDisposables.Add(spotTileViewModel.ObservePropertyChanged(x => x.FakeSpotPair)
.Where(x => !string.IsNullOrEmpty(x.NewValue))
.Subscribe(x =>
{
this.SetupFakePairSubscription();
}));
if (!string.IsNullOrEmpty(spotTileViewModel.FakeSpotPair))
{
SetupFakePairSubscription();
}
}
private void SetupFakePairSubscription()
{
if (spotTileViewModel == null)
return;
fakePairDisposables.Add(fakeSpotRateProvider.MonitorFakePair(spotTileViewModel.FakeSpotPair)
.Subscribe(x =>
{
if (spotTileViewModel.IsEnabled)
{
spotTileViewModel.RateViewModel.AcceptNewPrice(x);
}
}));
}
}
TimeoutBehaviour
当 ViewModel 选择了一个货币对后,这个行为的作用是启动一个计时器。超时将在超时结束后禁用 ViewModel
public class TimeoutBehaviour : ISpotTileViewModelBehaviour
{
private SpotTileViewModel spotTileViewModel;
private CompositeDisposable disposables = new CompositeDisposable();
private CompositeDisposable fakePairDisposables = new CompositeDisposable();
public TimeoutBehaviour()
{
}
public void Dispose()
{
disposables.Dispose();
fakePairDisposables.Dispose();
}
public void Start(SpotTileViewModel spotTileViewModel)
{
this.spotTileViewModel = spotTileViewModel;
SetupTopLevelSubscription();
}
private void SetupTopLevelSubscription()
{
//listen for changes in the number of legs
DisposableHelper.CreateNewCompositeDisposable(ref fakePairDisposables);
fakePairDisposables.Add(spotTileViewModel
.ObservePropertyChanged(x => x.FakeSpotPair)
.Where(x => !string.IsNullOrEmpty(x.NewValue))
.Subscribe(x =>
{
this.SetupFakePairSubscription();
}));
if (!string.IsNullOrEmpty(spotTileViewModel.FakeSpotPair))
{
SetupFakePairSubscription();
}
}
private void SetupFakePairSubscription()
{
if (spotTileViewModel == null)
return;
SetupTimeOutSubscription();
}
private void SetupTimeOutSubscription()
{
//this.disposables.Dispose();
spotTileViewModel.StartedTiming = true;
int counter = 0;
UpdateProgress(counter);
var tileDisabledObservable = spotTileViewModel
.ObservePropertyChanged(x => x.IsEnabled)
.Where(x => !x.NewValue);
this.disposables.Add(Observable.Interval(
TimeSpan.FromSeconds(Globals.ProgressTimeOut))
.TakeUntil(tileDisabledObservable)
.Subscribe(x =>
{
counter++;
UpdateProgress(counter);
if (counter == Globals.ProgressSegments - 1)
{
spotTileViewModel.IsEnabled = false;
this.disposables.Dispose();
}
}));
}
private void UpdateProgress(int counter)
{
var timeRemaining = (Globals.TotalTimeoutInSeconds -
(Globals.ProgressTimeOut * (counter + 1)));
spotTileViewModel.TimeOutRemaining = string.Format("{0}s of {1}s",
counter == 0 ? Globals.TotalTimeoutInSeconds : timeRemaining,
Globals.TotalTimeoutInSeconds);
//100/60 * 30 = 50% done
var secPerSegment = Globals.TotalTimeoutInSeconds/Globals.ProgressSegments;
spotTileViewModel.Progress =
(100/Globals.TotalTimeoutInSeconds) * (secPerSegment * counter+1);
}
}
就这些
好了,这就是这次我想说的全部内容。我目前正在写一篇关于 CQRS 的另一篇非常长的文章,我希望很快能发布。那需要一段时间才能完成,所以在此之前,如果喜欢这篇文章,能否请您花 2 分钟留下评论/问题,或投个票,它们总是受欢迎的。