组合 RX 和 MVVM






4.96/5 (15投票s)
一个使用 RX 的小型 MVVM 框架
引言
在我之前的一个职位上,我很幸运能够参与一个广泛使用 C# Reactive Extensions 的项目。 我也做了很多使用 MVVM 的 UI (WPF) 开发,所以我决定尝试将 MVVM 与 RX 结合起来。
为此,我想写一个简单的应用程序,但要涵盖 MVVM 的大部分方面。要实现的应用程序是一个地址簿,它具有以下功能:
- 所有人员将显示在主页的列表中
- 通过单击列表中项目上的 Details 按钮,可以查看/编辑所选人员的详细信息
- 通过单击列表中项目上的 Delete 按钮,可以从列表中删除所选人员
- 可以添加新人员
该应用程序将使用 Autofac 作为 IoC,并且所有依赖项都将在构造函数中注入。在 ViewModel 内部,属性和命令应该是可观察的。所以,我将首先从属性开始。由于属性是可读写的,因此它们需要实现 IObservable 和 IObserver 接口,以便我们可以推送新值并订阅值。幸运的是,RX 已经有一个定义好的接口,名为 ISubject。因此,我们的属性将扩展 ISubject 接口,并且只有一个名为 Value 的属性,看起来像这样:
public interface IPropertySubject<T> : ISubject<T>
{
T Value { get; set; }
}
实现将是这样的:
public class PropertySubject<T> : IPropertySubject<T>
{
private readonly Subject<T> _subject = new Subject<T>();
private T _value;
public void OnNext(T value)
{
SetValue(value);
}
private void SetValue(T value)
{
_value = value;
_subject.OnNext(value);
}
public void OnError(Exception error)
{
_subject.OnError(error);
}
public void OnCompleted()
{
_subject.OnCompleted();
}
public IDisposable Subscribe(IObserver<T> observer)
{
return _subject.Subscribe(observer);
}
public T Value
{
get { return _value; }
set { SetValue(value); }
}
}
从实现中,我们可以看到我们正在使用 RX 提供的 Subject 类实例。Subject 同时实现了 IObserver 和 IObservable。属性的值可以通过两种方式设置:一种是通过使用 OnNext() 方法,另一种是通过简单地设置 Value 属性。
下一个任务是实现命令。命令接口将实现 ICommand 并看起来像这样:
public interface ICommandObserver<T> : ICommand
{
IObservable<T> OnExecute { get; }
IObserver<bool> SetCanExecute { get; }
}
每次在 ICommand 上调用 Execute() 方法时,OnExecute 将向可观察对象推送一个 T 类型的新值。SetCanExecute 设置将由 ICommand 的 CanExecute 方法返回的值,并引发 CanExecuteChanged 事件(启用/禁用 UI 组件)。
ICommandObserver 的实现看起来像这样:
public class CommandObserver<T> : ICommandObserver<T>
{
private bool _canExecute;
private readonly ISubject<T> _executeSubject = new Subject<T>();
private readonly ISubject<bool> _canExecuteSubject = new Subject<bool>();
public CommandObserver() : this(true)
{
}
public CommandObserver(bool value)
{
_canExecute = value;
_canExecuteSubject.DistinctUntilChanged().Subscribe(v =>
{
_canExecute = v;
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
});
}
void ICommand.Execute(object parameter)
{
if(parameter is T)
_executeSubject.OnNext((T) parameter);
else
_executeSubject.OnNext(default(T));
}
bool ICommand.CanExecute(object parameter)
{
return _canExecute;
}
public event EventHandler CanExecuteChanged;
public IObservable<T> OnExecute
{
get { return _executeSubject.AsObservable(); }
}
public IObserver<bool> SetCanExecute
{
get { return _canExecuteSubject.AsObserver(); }
}
}
正如你所见,其中一个构造函数接受一个布尔值。这个值是 CanExecute() 方法返回的初始值(启用/禁用 UI 组件)。构造函数中发生的另一件事是,只有当 SetCanExecute 观察者上推送的值从 true 变为 false 或反之亦然时,我们才会引发 CanExecuteChanged 事件。因此,基本上,即使我们在 SetCanExecute 中推送了多个相同的值,该事件也只会被触发一次(这是 RX 使用 DistinctUntilChanged() 方法的另一个很酷的功能)。
现在我们创建一个所有视图模型都将继承的基视图模型。类定义如下:
public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
{
private readonly CompositeDisposable _disposables = new CompositeDisposable();
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public void ShouldDispose(IDisposable disposable)
{
_disposables.Add(disposable);
}
public void Dispose()
{
_disposables.Dispose();
}
}
当我们订阅一个可观察对象时,会返回一个 IDisposable。要取消订阅可观察对象,我们需要调用返回的 IDisposable 上的 Dispose() 方法。由于我们视图模型中的所有属性和命令都将是可观察的,因此每次订阅可观察对象时,我们都需要跟踪返回的可处置项,以便在视图模型被处置时,我们需要取消订阅所有可观察对象。我们通过将返回的可处置项(订阅时)添加到 CompositeDisposable 实例(_disposables 对象)来实现这一点。CompositeDisposable 是 RX 中的另一个类,它是一个可处置项列表,当调用其 Dispose() 方法时,它将调用其包含的所有项的 Dispose() 方法。
下一步是创建一个属性提供程序,它将创建在视图模型接口中声明的属性或命令。因此,属性提供程序将看起来像这样:
public interface IPropertyProvider<T> : IDisposable
{
IPropertySubject<K> CreateProperty<K>(Expression<Func<T, K>> expression);
IPropertySubject<K> CreateProperty<K>(Expression<Func<T, K>> expression, K value);
IPropertySubject<K> CreateProperty<K>(Expression<Func<T, K>> expression, IObservable<K> values);
ICommandObserver<K> CreateCommand<K>(Expression<Func<T, ICommand>> expression);
ICommandObserver<K> CreateCommand<K>(Expression<Func<T, ICommand>> expression, bool isEnabled);
ICommandObserver<K> CreateCommand<K>(Expression<Func<T, ICommand>> expression, IObservable<bool> isEnabled);
}
该接口声明了三个创建属性的重载方法。第一个方法返回正确属性类型的新实例,第二个方法返回属性的新实例并将其初始值设置为提供的,第三个方法返回属性的实例并将其值设置为提供的可观察对象的值(每次有新元素进入可观察对象时更新值)。创建命令的方法也是如此。
属性提供程序接口的实现看起来像这样:
public class PropertyProvider<T> : IPropertyProvider<T>
{
private readonly ViewModelBase _viewModel;
private readonly ISchedulers _schedulers;
private readonly CompositeDisposable _disposables = new CompositeDisposable();
public PropertyProvider(ViewModelBase viewModel, ISchedulers schedulers)
{
_viewModel = viewModel;
_schedulers = schedulers;
_viewModel.ShouldDispose(_disposables);
}
private PropertySubject<K> GetProperty<K>(Expression<Func<T, K>> expr)
{
var propertyName = ((MemberExpression) expr.Body).Member.Name;
var propSubject = new PropertySubject<K>();
_disposables.Add(propSubject.ObserveOn(_schedulers.Dispatcher)
.Subscribe(v => _viewModel.OnPropertyChanged(propertyName)));
return propSubject;
}
public void Dispose()
{
if(!_disposables.IsDisposed)
_disposables.Dispose();
}
public IPropertySubject<K> CreateProperty<K>(Expression<Func<T, K>> expression)
{
return GetProperty(expression);
}
public IPropertySubject<K> CreateProperty<K>(Expression<Func<T, K>> expression, K value)
{
var propSubject = GetProperty(expression);
propSubject.Value = value;
return propSubject;
}
public IPropertySubject<K> CreateProperty<K>(Expression<Func<T, K>> expression, IObservable<K> values)
{
var propSubject = GetProperty(expression);
_disposables.Add(values.Subscribe(v => propSubject.Value = v));
return propSubject;
}
public ICommandObserver<K> CreateCommand<K>(Expression<Func<T, ICommand>> expression)
{
return new CommandObserver<K>(true);
}
public ICommandObserver<K> CreateCommand<K>(Expression<Func<T, ICommand>> expression, bool isEnabled)
{
return new CommandObserver<K>(isEnabled);
}
public ICommandObserver<K> CreateCommand<K>(Expression<Func<T, ICommand>> expression, IObservable<bool> isEnabled)
{
var cmd = new CommandObserver<K>(true);
_disposables.Add(isEnabled.Subscribe(cmd.SetCanExecute));
return cmd;
}
}
从实现中,我们可以看到 PropertyProvider 在其构造函数中接受 ViewModelBase 和 ISchedulers 的实例。ISchedulers(定义和实现将在后面给出)只是 RX 提供的不同调度程序(Thread, ThreadPool, Task, Dispatcher, ...)的一个包装器。
正如我们从 GetProperty<>(...) 方法中看到的,每次创建新的属性主题时,我们都会订阅它,因此每当有新值推送到它时,PropertyChanged 事件都会在视图模型上自动触发,因此我们在视图模型中无需担心引发 PropertyChanged 事件。RX 提供的另一个很酷的功能是,由于我们在调度程序调度程序上观察(使用 RX 提供的 ObserveOn(...) 方法),我们可以在任何线程上更改属性的值,并且属性将在调度程序上更新。因此,不需要丑陋的 InvokeRequired 或 CheckAccess() 和 BeginInvoke() 检查。
现在,我们不再在视图模型中为属性创建 PropertyProvider 类的具体实例,而是将属性提供程序的创建推迟给一个工厂类,然后将此工厂注入到我们视图模型的构造函数中。这允许我们在编写单元测试时模拟工厂,并且我们也遵守视图模型中的 SRP(单一职责原则)(这是我在我的项目中使用的模式,尤其是在使用 IoC 时)。
因此,工厂的接口看起来像这样:
public interface IPropertyProviderFactory
{
IPropertyProvider<T> Create<T>(ViewModelBase viewModelBase);
}
而实现看起来像这样:
public class PropertyProviderFactory : IPropertyProviderFactory
{
private readonly ISchedulers _schedulers;
public PropertyProviderFactory(ISchedulers schedulers)
{
_schedulers = schedulers;
}
public IPropertyProvider<T> Create<T>(ViewModelBase viewModelBase)
{
return new PropertyProvider<T>(viewModelBase, _schedulers);
}
}
接下来的要实现的是一个消息总线,它将由视图模型用于相互通信(类似于事件聚合器模式)。消息总线接口非常简单:
public interface IMessageBus
{
IDisposable Subscribe<T>(Action<T> action);
void Publish<T>(T item);
}
因此,如果我们想发布一条消息,我们会通过提供消息类型的实例来使用 Publish(...) 方法(消息被声明为类),如果我们想收听特定类型的消息,我们会订阅相关类型的消息并传递一个将在消息发布时执行的动作。
在我们可以继续我们的应用程序之前,最后剩下的是 ISchedulers 接口的签名和实现,它看起来像这样:
public interface ISchedulers
{
IScheduler Dispatcher { get; }
IScheduler NewThread { get; }
IScheduler NewTask { get; }
IScheduler ThreadPool { get; }
IScheduler Timer { get; }
}
And the implementation:
public class Schedulers : ISchedulers
{
public IScheduler Dispatcher
{
get { return DispatcherScheduler.Instance; }
}
public IScheduler NewThread
{
get { return Scheduler.NewThread; }
}
public IScheduler NewTask
{
get { return Scheduler.ThreadPool; }
}
public IScheduler ThreadPool
{
get { return Scheduler.ThreadPool; }
}
public IScheduler Timer
{
get { return Scheduler.Immediate; }
}
}
如前所述,定义 ISchedulers 接口而不是使用 RX 调度程序的原因是,当我们编写单元测试时,我们可以模拟 ISchedulers 接口并返回测试调度程序。
现在我们剩下实现应用程序本身。首先,我们将创建一个用作对话框窗口以显示人员详细信息的窗口。这个窗口是一个标准的 WPF 窗口。但是,我们希望这个窗口足够通用,以便它可以显示我们传递给它的任何视图模型,因此我们需要修改构造函数。类(在代码隐藏中)的定义如下:
public partial class DialogWindow : Window
{
public DialogWindow(ViewModelBase viewModel, ISchedulers schedulers)
{
InitializeComponent();
DataContext = viewModel;
var closeable = viewModel as ICloseable;
if (closeable != null)
closeable.Close.ObserveOn(schedulers.Dispatcher).Subscribe(r =>
{
CloseResult = r;
Close();
});
}
public bool CloseResult { get; private set; }
}
而 XAML 看起来像这样:
<Window x:Class="Test_Mvvm.DialogWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:Test_Mvvm.Views"
xmlns:viewModels="clr-namespace:Test_Mvvm.ViewModels"
Title="DialogWindow" Height="300" Width="300" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen"
ResizeMode="NoResize" Background="#FF737D81">
<Window.Resources>
<DataTemplate DataType="{x:Type viewModels:PersonViewModel}">
<views:PersonDetailsView />
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding}" />
</Grid>
</Window>
从实现中,我们可以看到构造函数接受两个参数,第一个参数是 ViewModelBase 的实例,第二个参数是 ISchedulers 的实例。然后,我们将窗口的 DataContext 设置为视图模型,并在 XAML 中定义一个用于渲染的 DataTemplate。在我们的例子中,只有 PersonViewModel 需要显示。然后,我们检查提供的视图模型是否实现了 ICloseable 接口,该接口声明为:
public interface ICloseable
{
IObservable<bool> Close { get; }
}
如果视图模型实现了该接口,那么窗口将订阅其 Close 可观察属性,并且每当有值推送到 Close 可观察对象时,窗口就会关闭。这是为了使视图模型能够关闭窗口,因为窗口对我们的视图模型或视图一无所知(它使用定义的 DataTemplate 来渲染视图模型,仅此而已)。 因此,在我们的例子中,当用户单击详细信息视图上的 Cancel 或 Save 按钮时,对话框就需要关闭。再次,我们使用调度程序将窗口的关闭安排在调度程序调度程序上进行,从而允许我们从任何线程将值推送到 Close 可观察对象。
现在我们将开始实现我们应用程序的视图模型,我将从 person 视图模型接口开始。接口定义如下:
public interface IPersonViewModel : IDisposable
{
string Name { get; }
string Surname { get; }
int Age { get; }
string Address1 { get; }
string Address2 { get; }
string Address3 { get; }
string City { get; }
ICommand EditCommand { get; }
ICommand SaveCommand { get; }
ICommand DeleteCommand { get; }
ICommand CloseCommand { get; }
}
下一步是创建可用于 Visual Studio WPF 设计器或 Expression Blend 的设计数据。设计数据是一个实现 IPersonViewModel 接口的类,它是一个具有一些初始值设置的简单类。设计数据类的实现如下:
public class DPersonViewModel : IPersonViewModel
{
public DPersonViewModel()
{
Name = "Joe";
Surname = "Bloggs";
Age = 40;
Address1 = "Flat 1";
Address2 = "Calder court";
Address3 = "253 Rotherhithe Street";
City = "London";
}
public string Name { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string Address3 { get; set; }
public string City { get; set; }
public ICommand EditCommand { get; set; }
public ICommand SaveCommand { get; set; }
public ICommand DeleteCommand { get; set; }
public ICommand CloseCommand { get; set; }
public void Dispose()
{
}
}
现在我们需要构建视图。我们有两个不同的视图来显示 PersonViewModel,一个用于显示/编辑详细信息,另一个用于在列表中显示人员数据。我们将首先从列表项开始,它将被称为 PersonSummaryView。该视图的 xaml 如下所示:
<UserControl x:Class="Test_Mvvm.Views.PersonSummaryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dd="clr-namespace:Test_Mvvm.DesignData"
mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=dd:DPersonViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="55" d:DesignWidth="500" BorderBrush="DarkGray" BorderThickness="1" Margin="5" Background="#33FFFFFF" >
<UserControl.Resources>
<ControlTemplate x:Key="LinkButton" TargetType="{x:Type Button}">
<TextBlock HorizontalAlignment="Center" Margin="0" Text="{TemplateBinding Content}"
Cursor="Hand" TextWrapping="Wrap" VerticalAlignment="Center"
TextDecorations="Underline" Foreground="#FF0025BA" />
</ControlTemplate>
</UserControl.Resources>
<Grid Margin="5">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Bold" />
<TextBlock Text=" " FontSize="13" FontWeight="Bold" />
<TextBlock Text="{Binding Surname}" FontSize="13" FontWeight="Bold"/>
</StackPanel>
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,70,0" Width="130">
<TextBlock Text="{Binding Address1}" FontSize="8" HorizontalAlignment="Center" />
<TextBlock Text="{Binding Address2}" FontSize="8" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Address3}" FontSize="8" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding City}" FontSize="8" HorizontalAlignment="Center"/>
</StackPanel>
<Button Template="{StaticResource LinkButton}" Command="{Binding EditCommand}"
Content="Details" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,3,5,0" />
<Button Template="{StaticResource LinkButton}" Command="{Binding DeleteCommand}"
Content="Delete" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,5,3" />
</Grid>
</UserControl>
以及 Visual Studio 设计器中的控件如下所示:
正如我们所见,设计数据绑定到不同的控件并在设计器中显示,这使得控件的样式设置非常容易。视图的设计数据是通过在开头使用 d:DataContext="{d:DesignInstance Type=dd:DPersonViewModel, IsDesignTimeCreatable=True}" 来设置的。
接下来,我们设计用于显示人员详细信息的视图。再次,我们将使用相同的设计数据来设置我们的视图样式。样式设置后,视图的 xaml 如下所示:
<UserControl x:Class="Test_Mvvm.Views.PersonDetailsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dd="clr-namespace:Test_Mvvm.DesignData"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" d:DataContext="{d:DesignInstance Type=dd:DPersonViewModel, IsDesignTimeCreatable=True}"
Height="260" Width="330">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="85*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="Name :" Grid.Column="0" Grid.Row="0" VerticalAlignment="Center"/>
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="0" VerticalAlignment="Center" />
<TextBlock Text="Surname :" Grid.Column="0" Grid.Row="1" VerticalAlignment="Center"/>
<TextBox Text="{Binding Surname, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="1" VerticalAlignment="Center" />
<TextBlock Text="Address1 :" Grid.Column="0" Grid.Row="2" VerticalAlignment="Center"/>
<TextBox Text="{Binding Address1, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="2" VerticalAlignment="Center" />
<TextBlock Text="Address2 :" Grid.Column="0" Grid.Row="3" VerticalAlignment="Center"/>
<TextBox Text="{Binding Address2, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="3" VerticalAlignment="Center" />
<TextBlock Text="Address3 :" Grid.Column="0" Grid.Row="4" VerticalAlignment="Center"/>
<TextBox Text="{Binding Address3, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="4" VerticalAlignment="Center" />
<TextBlock Text="City :" Grid.Column="0" Grid.Row="5" VerticalAlignment="Center"/>
<TextBox Text="{Binding City, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="5" VerticalAlignment="Center" />
<TextBlock Text="Age :" Grid.Column="0" Grid.Row="6" VerticalAlignment="Center"/>
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="6" VerticalAlignment="Center" />
<Button Content="Save" Command="{Binding SaveCommand}" Grid.Column="1" Grid.Row="7" VerticalAlignment="Bottom" HorizontalAlignment="Right"
Width="80" Margin="0,0,95,0"/>
<Button Content="Cancel" Command="{Binding CloseCommand}" Grid.Column="1" Grid.Row="7" VerticalAlignment="Bottom" HorizontalAlignment="Right"
Width="80" Margin="0,0,5,0" />
</Grid>
</UserControl>
以及设计器上的控件将如下所示:
下一步是定义人员的数据模型。模型用于从存储库(数据库)检索数据并将数据发送到存储库。数据模型将如下所示:
public class Person
{
public string Name { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string Address3 { get; set; }
public string City { get; set; }
}
现在,在我们深入实现 person 视图模型之前,让我们先描述一下这个视图模型将处理的任务。首先,视图模型将获取一个数据模型并用模型值填充属性。然后,当用户单击摘要视图(PersonSummeryView)上的 Details 按钮时,视图模型应弹出带有详细信息的对话框,以便用户可以查看/编辑详细信息。如果用户在详细信息对话框中更改了详细信息并单击 Save 按钮,则应更新模型并将模型保存到存储库。否则,如果用户单击 Cancel 按钮,所有更改都将被撤销,对话框将关闭。
接下来,当用户单击摘要视图上的 Delete 按钮时,该项目应从存储库中删除,并且该项目应从列表中移除。现在,因为视图模型不知道容器(在我们的例子中是列表),它不知道如何删除自身并将其从列表中移除。因此,而不是将父视图模型传递给它并将它们紧密耦合在一起,视图模型将发布一条消息到 MessageBus,任何收听该消息的人都拥有删除视图模型并将其从列表中移除所需的知识。
因此,视图模型的实现看起来像这样:
public class PersonViewModel : ViewModelBase, IPersonViewModel, ICloseable
{
private Person _personModel;
private readonly IPropertySubject<string> _name;
private readonly IPropertySubject<string> _surname;
private readonly IPropertySubject<string> _address1;
private readonly IPropertySubject<int> _age;
private readonly IPropertySubject<string> _address2;
private readonly IPropertySubject<string> _address3;
private readonly IPropertySubject<string> _city;
private readonly ICommandObserver<Unit> _editCommand;
private readonly ICommandObserver<Unit> _saveCommand;
private readonly ICommandObserver<Unit> _deleteCommand;
private readonly ISubject<bool> _closeSubject = new Subject<bool>();
private readonly ICommandObserver<Unit> _closeCommand;
public PersonViewModel(IPropertyProviderFactory providerFactory,
IMessageBus messageBus,
IDialogService dialogService,
Person personModel)
{
_personModel = personModel;
var pp = providerFactory.Create<IPersonViewModel>(this);
// creating properties
_name = pp.CreateProperty(i => i.Name, personModel.Name);
_surname = pp.CreateProperty(i => i.Surname, personModel.Surname);
_age = pp.CreateProperty(i => i.Age, personModel.Age);
_address1 = pp.CreateProperty(i => i.Address1, personModel.Address1);
_address2 = pp.CreateProperty(i => i.Address2, personModel.Address2);
_address3 = pp.CreateProperty(i => i.Address3, personModel.Address3);
_city = pp.CreateProperty(i => i.City, personModel.City);
// creating commands
_editCommand = pp.CreateCommand<Unit>(i => i.EditCommand);
_saveCommand = pp.CreateCommand<Unit>(i => i.SaveCommand, !string.IsNullOrEmpty(personModel.Name) && !string.IsNullOrEmpty(personModel.Surname));
_deleteCommand = pp.CreateCommand<Unit>(i => i.DeleteCommand);
_closeCommand = pp.CreateCommand<Unit>(i => i.CloseCommand);
ShouldDispose(_name.CombineLatest(_surname, (n, s) => !string.IsNullOrEmpty(n) && !string.IsNullOrEmpty(s))
.Subscribe(_saveCommand.SetCanExecute));
ShouldDispose(_saveCommand.OnExecute.Subscribe(_ =>
{
UpdateModel();
_closeSubject.OnNext(true);
}));
ShouldDispose(_editCommand.OnExecute.Subscribe(_ => dialogService.ShowViewModel("Edit Person", this)));
ShouldDispose(_deleteCommand.OnExecute.Subscribe(_ => messageBus.Publish(new DeleteMessage<PersonViewModel>(this))));
ShouldDispose(_closeCommand.OnExecute.Subscribe(_ =>
{
ResetData();
_closeSubject.OnNext(false);
}));
}
public string Name
{
get { return _name.Value; }
set { _name.Value = value; }
}
public string Surname
{
get { return _surname.Value; }
set { _surname.Value = value; }
}
public int Age
{
get { return _age.Value; }
set { _age.Value = value; }
}
public string Address1
{
get { return _address1.Value; }
set { _address1.Value = value; }
}
public string Address2
{
get { return _address2.Value; }
set { _address2.Value = value; }
}
public string Address3
{
get { return _address3.Value; }
set { _address3.Value = value; }
}
public string City
{
get { return _city.Value; }
set { _city.Value = value; }
}
public ICommand EditCommand
{
get { return _editCommand; }
}
public ICommand SaveCommand
{
get { return _saveCommand; }
}
public ICommand DeleteCommand
{
get { return _deleteCommand; }
}
public ICommand CloseCommand
{
get { return _closeCommand; }
}
IObservable<bool> ICloseable.Close
{
get { return _closeSubject; }
}
private void UpdateModel()
{
_personModel = this.ToModel();
}
private void ResetData()
{
Name = _personModel.Name;
Surname = _personModel.Surname;
Address1 = _personModel.Address1;
Address2 = _personModel.Address2;
Address3 = _personModel.Address3;
City = _personModel.City;
Age = _personModel.Age;
}
}
正如我们所见,所有操作都在构造函数中完成,因此如果我们开始分解构造函数,我们可以看到我们最初创建属性并从数据模型设置初始值:
_name = pp.CreateProperty(i => i.Name, personModel.Name);
_surname = pp.CreateProperty(i => i.Surname, personModel.Surname);
_age = pp.CreateProperty(i => i.Age, personModel.Age);
_address1 = pp.CreateProperty(i => i.Address1, personModel.Address1);
_address2 = pp.CreateProperty(i => i.Address2, personModel.Address2);
_address3 = pp.CreateProperty(i => i.Address3, personModel.Address3);
_city = pp.CreateProperty(i => i.City, personModel.City);
然后我们创建命令:
_editCommand = pp.CreateCommand<Unit>(i => i.EditCommand);
_saveCommand = pp.CreateCommand<Unit>(i => i.SaveCommand, !string.IsNullOrEmpty(personModel.Name) && !string.IsNullOrEmpty(personModel.Surname));
_deleteCommand = pp.CreateCommand<Unit>(i => i.DeleteCommand);
_closeCommand = pp.CreateCommand<Unit>(i => i.CloseCommand);
正如我们所见,保存命令最初将在数据模型上的 Name 和 Surname 属性不为 null 或空时启用。
在下一行,我们使用 CombineLatest() 方法组合 _name 和 _surname 可观察对象,然后通过提供 save 命令的 SetCanExecute 属性(它是一个观察者)来订阅结果。因此,每当任一 name 或 surname 可观察对象上推送到新值时,提供的函数将获取两个可观察对象的值并返回另一个值(在我们的例子中,它将根据 name 和 surname 的值是否不为 null 或空生成一个布尔值)。然后,函数生成的结果将被推送到一个可观察对象,该可观察对象又会被推送到 SetCanExecute,从而根据结果启用/禁用控件。
接下来,在以下代码块中:
ShouldDispose(_saveCommand.OnExecute.Subscribe(_ =>
{
UpdateModel();
_closeSubject.OnNext(true);
}));
我们订阅 _saveCommand.OnExecute,并且每当执行 SaveCommand 命令时(在我们的例子中,通过单击详细信息视图上的 Save 按钮),我们都会更新数据模型并将值推送到 Close 可观察对象,从而关闭对话框。
在下一行:
ShouldDispose(_editCommand.OnExecute.Subscribe(_ => dialogService.ShowViewModel("Edit Person", this)));
我们订阅 _editCommand.OnExecute,并且每当执行 edit 命令时(通过单击摘要视图上的 Details 按钮),我们不是在视图模型中自己创建对话框,而是将其委托给对话框服务(通过调用 ShowViewModel() 方法)。通过这种方式,正如我们之前所说,我们在编写单元测试时可以模拟对话框服务,并且我们也遵守了视图模型中的 SRP 原则。
然后,在下一行:
ShouldDispose(_deleteCommand.OnExecute.Subscribe(_ => messageBus.Publish(new DeleteMessage<PersonViewModel>(this))));
我们订阅 delete 命令,并且每当它被执行时(通过单击摘要视图上的 Delete 按钮),我们将发布一个新的 DeleteMessage 到消息总线。因此,任何收听此消息的人都可以做出响应。
最后,在下一个块中:
ShouldDispose(_closeCommand.OnExecute.Subscribe(_ =>
{
ResetData();
_closeSubject.OnNext(false);
}));
我们订阅 Close 命令,因此每当用户单击详细信息视图上的 Close 按钮时,我们将撤销属性值并关闭对话框(通过将值推送到 Close 可观察对象,该对象在 ICloseable 接口中定义并在本视图模型中实现)。
下一步是定义和实现对话框服务。对话框服务的定义如下:
public interface IDialogService
{
void ShowInfo(string title, string message);
MessageBoxResult ShowWarning(string title, string message);
void ShowError(string title, string message);
bool ShowViewModel(string title, ViewModelBase viewModel);
}
而实现看起来像这样:
public class DialogService : IDialogService
{
private readonly ISchedulers _schedulers;
public DialogService(ISchedulers schedulers)
{
_schedulers = schedulers;
}
public void ShowInfo(string title, string message)
{
MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
}
public MessageBoxResult ShowWarning(string title, string message)
{
return MessageBox.Show(message, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning);
}
public void ShowError(string title, string message)
{
MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Error);
}
public bool ShowViewModel(string title, ViewModelBase viewModel)
{
var win = new DialogWindow(viewModel, _schedulers) { Title = title };
win.ShowDialog();
return win.CloseResult;
}
}
现在我们需要定义和实现将保存人员对象的存储库。存储库的定义如下:
public interface IPersonRepository
{
IObservable<IEnumerable<Person>> GetAllPersons();
IObservable<bool> Save(Person person);
IObservable<bool> Delete(Person person);
}
正如我们从接口定义中看到的,所有方法都声明为可观察对象。这允许我们订阅方法并在获取数据时做出响应,使方法异步。为了本应用程序的目的,我们只会在内存中将数据保存在列表中,但对于实际应用程序,我们会实现此接口,以便数据存储在数据库中。存储库接口的实现看起来像这样:
public class PersonRepository : IPersonRepository
{
private readonly List<Person> _persons;
public PersonRepository()
{
_persons = new List<Person>
{
new Person { Name = "Joe", Surname = "Blogs", Address1 = "Address1", Address2 = "Address2", Address3 = "Address3", Age = 21, City = "London"},
new Person { Name = "Mary", Surname = "Jones", Address1 = "Address1", Address2 = "Address2", Address3 = "Address3", Age = 28, City = "New York"},
};
}
public IObservable<IEnumerable<Person>> GetAllPersons()
{
return ToObservable(() =>
{
IEnumerable<Person> cl = _persons;
Thread.Sleep(1000);
return cl;
});
}
public IObservable<bool> Save(Person person)
{
return ToObservable(() =>
{
_persons.Add(person);
Thread.Sleep(1000);
return true;
});
}
public IObservable<bool> Delete(Person person)
{
return ToObservable(() =>
{
var cl = _persons.FirstOrDefault(c => c.Name == person.Name && c.Surname == person.Surname);
if (cl != null)
{
_persons.Remove(cl);
return true;
}
return false;
});
}
private IObservable<T> ToObservable<T>(Func<T> func)
{
return func.ToAsync()();
}
}
正如我们从私有方法 ToObservable<>() 的实现中看到的,RX 还有另一个很酷的功能,可以将函数转换为返回可观察对象的异步函数。为了模仿长时间运行的过程,我们在调用 GetAllPersons() 和 Save() 方法时还添加了 1 秒的 Thread.Sleep。
下一步是定义主视图模型。主视图模型将负责从存储库获取所有人员并在列表视图中显示它们,添加新人员,以及从列表中删除相应的人员。因此,定义如下:
public interface IMainViewModel : IDisposable
{
ObservableCollection<IPersonViewModel> Clients { get; }
ICommand AddNewClientCommand { get; }
bool IsBusy { get; }
}
而实现看起来像这样:
public class MainViewModel : ViewModelBase, IMainViewModel
{
private readonly ICommandObserver<Unit> _addNewClientCommand;
private readonly IPropertySubject<bool> _isBusy;
public MainViewModel(IPropertyProviderFactory providerFactory,
IMessageBus messageBus,
ISchedulers schedulers,
IPersonViewModelFactory personViewModelFactory,
IDialogService dialogService,
IPersonRepository personRepository)
{
var pp = providerFactory.Create<IMainViewModel>(this);
_isBusy = pp.CreateProperty(i => i.IsBusy, false);
_addNewClientCommand = pp.CreateCommand<Unit>(i => i.AddNewClientCommand);
Clients = new ObservableCollection<IPersonViewModel>();
IsBusy = true;
ShouldDispose(personRepository.GetAllClients()
.ObserveOn(schedulers.Dispatcher)
.Subscribe(c =>
{
foreach(var item in c.Select(personViewModelFactory.Create))
Clients.Add(item);
IsBusy = false;
}));
ShouldDispose(messageBus.Subscribe<DeleteMessage<PersonViewModel>>(m =>
{
var msg = string.Format("Are you sure you want to delete {0} {1}?", m.ViewModel.Name, m.ViewModel.Surname);
if (dialogService.ShowWarning("Delete", msg) == MessageBoxResult.OK)
{
IsBusy = true;
ShouldDispose(personRepository.Delete(m.ViewModel.ToModel())
.ObserveOn(schedulers.Dispatcher)
.Subscribe(_ =>
{
Clients.Remove(m.ViewModel);
IsBusy = false;
}));
}
}));
ShouldDispose(_addNewClientCommand.OnExecute.Subscribe(p =>
{
var newClient = personViewModelFactory.Create(new Person());
if (dialogService.ShowViewModel("New person", newClient as ViewModelBase))
{
IsBusy = true;
ShouldDispose(personRepository.Save(newClient.ToModel())
.ObserveOn(schedulers.Dispatcher)
.Subscribe(_ =>
{
IsBusy = false;
Clients.Add(newClient);
}));
}
}));
}
public ObservableCollection<IPersonViewModel> Clients { get; private set; }
public bool IsBusy
{
get { return _isBusy.Value; }
set { _isBusy.Value = value; }
}
public ICommand AddNewClientCommand
{
get { return _addNewClientCommand; }
}
}
所以,如果我们再次分解构造函数,我们将看到一开始我们创建了属性:
_isBusy = pp.CreateProperty(i => i.IsBusy, false);
_addNewClientCommand = pp.CreateCommand<Unit>(i => i.AddNewClientCommand);
Clients = new ObservableCollection<IPersonViewModel>();
在下一个代码块中,我们从存储库检索所有人员:
ShouldDispose(personRepository.GetAllClients()
.ObserveOn(schedulers.Dispatcher)
.Subscribe(c =>
{
foreach(var item in c.Select(personViewModelFactory.Create))
Clients.Add(item);
IsBusy = false;
}));
然后,在下一个块中,我们订阅消息总线上的 DeleteMessage,并且每当消息总线上发布 delete 消息时,我们通过从列表中删除选定人员来响应它:
ShouldDispose(messageBus.Subscribe<DeleteMessage<PersonViewModel>>(m =>
{
var msg = string.Format("Are you sure you want to delete {0} {1}?", m.ViewModel.Name, m.ViewModel.Surname);
if (dialogService.ShowWarning("Delete", msg) == MessageBoxResult.OK)
{
IsBusy = true;
ShouldDispose(personRepository.Delete(m.ViewModel.ToModel())
.ObserveOn(schedulers.Dispatcher)
.Subscribe(_ =>
{
Clients.Remove(m.ViewModel);
IsBusy = false;
}));
}
}));
最后,我们订阅 _addNewClient 命令,因此每当用户单击按钮时,我们都会显示对话框供用户输入详细信息,一旦输入了详细信息并且用户单击 Save 按钮,新人员就会存储在存储库中。
ShouldDispose(_addNewClientCommand.OnExecute.Subscribe(p =>
{
var newClient = personViewModelFactory.Create(new Person());
if (dialogService.ShowViewModel("New person", newClient as ViewModelBase))
{
IsBusy = true;
ShouldDispose(personRepository.Save(newClient.ToModel())
.ObserveOn(schedulers.Dispatcher)
.Subscribe(_ =>
{
IsBusy = false;
Clients.Add(newClient);
}));
}
}));
我们在应用程序中要做的最后一件事是在 IoC 容器中注册类型,并设置主窗口的 DataContext。我们在 App.xaml 的代码隐藏中执行此操作。我们的代码隐藏代码如下:
public partial class App : Application
{
private IContainer _container;
private readonly ContainerBuilder _containerBuilder;
public App()
{
_containerBuilder = new ContainerBuilder();
// Register MVVM dependencies
containerBuilder.RegisterType<PropertyProviderFactory>().As<IPropertyProviderFactory>().SingleInstance();
containerBuilder.RegisterType<Schedulers>().As<ISchedulers>().SingleInstance();
containerBuilder.RegisterType<MessageBus>().As<IMessageBus>().SingleInstance();
//Register ViewModels
_containerBuilder.RegisterType<MainViewModel>().As<IMainViewModel>().SingleInstance();
_containerBuilder.RegisterType<PersonViewModelFactory>().As<IPersonViewModelFactory>().SingleInstance();
_containerBuilder.RegisterType<DialogService>().As<IDialogService>().SingleInstance();
_containerBuilder.RegisterType<PersonRepository>().As<IPersonRepository>().SingleInstance();
}
protected override void OnStartup(StartupEventArgs e)
{
try
{
ShutdownMode = ShutdownMode.OnMainWindowClose;
_container = _containerBuilder.Build();
var mainWindow = new MainWindow { DataContext = _container.Resolve<IMainViewModel>() };
MainWindow = mainWindow;
mainWindow.Show();
base.OnStartup(e);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
作为示例构建的应用程序非常简单,但它让你对 RX 的功能有所了解。RX 是一个非常强大的框架,可以简化编程世界的许多事情,尤其是异步编程。
历史
N/A