[RxUI] 11 – 消息总线
像许多其它MVVM框架一样,ReactiveUI也包括消息总线模式的实现。这样,就可以在代码的不同部分之间发送和接收消息,而无需彼此直接访问。
ReactiveUI中默认MessageBus(MessageBus.Current)的一个独特属性是它通过UI线程调度消息。这意味着从后台线程发送的消息将自动到达主线程。MessageBus也可用于在代码的不同层之间封送消息(通常是将消息从View发送到ViewModel)。
虽然使用此类是因为有时它是必需的,但是应将MessageBud作为最后的手段。MessageBus实际上是一个全局变量,这意味着它易受内存和事件泄漏的影响,此外,MessageBus的分离性意味着它是一个目的地不可见的goto
。其还鼓励不良设计,因为很多人将View事件直接代理到ViewModel层,这使它们不是特别的ViewModelly。
基础
MessageBus非常简单。首先,设置一个listener:
// Listen for anyone sending instances of the KeyUpEventArgs class. Since
// MessageBus simply returns an IObservable, it can be combined or used in
// many different ways
MessageBus.Current.Listen<KeyUpEventArgs>()
.Where(e => e.KeyCode == KeyCode.Up)
.Subscribe(x => Console.WriteLine("Up Pressed!"));
现在,通过RegisterMessageSource
将IObservable连接到总线:
MessageBus.Current.RegisterMessageSource(RootVisual.Events().KeyUpObs);
或者,手动发送消息:
MessageBus.Current.SendMessage(new KeyUpEventArgs());
避免使用MessageBus
与其它MVVM框架不同,ReactiveUI的独创性,通常会有更多正确的方法来解决问题。即使对象是随时间变化的,WhenAny
和WhenAnyObservable
也可以用来描述如何到达这些对象。通常在视图中最有用:
public LoginView()
{
// As soon as the CredentialsAreValid turns to 'true', set the focus
// to the Ok button.
this.WhenAny(x => x.ViewModel.CredentialsAreValid, x => x.Value)
.Where(x => x != false)
.Subscribe(_ => OkButton.SetFocus());
}
考虑另一种情况,一个包含Document ViewModels列表的用来打开文档的ViewModel—每个Document包含一个Close
命令。MVVM的许多传统实现都难以实现此命令,要么保留对列表的引用,要么通过MessageBus。
但是,除此之外,还可以使用Rx的操作符来以更优雅的方式解决此问题。
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
public class DocumentViewModel : ReactiveObject
{
public ReactiveCommand<Unit, Unit> Close { get; set; }
public DocumentViewModel()
{
// Note that we don't actually *subscribe* to Close here or implement
// anything in DocumentViewModel, because Closing is a responsibility
// of the document list.
Close = ReactiveCommand.Create(() => { });
}
}
public class MainViewModel : ReactiveObject
{
public ObservableCollection<DocumentViewModel> OpenDocuments { get; protected set; }
public MainViewModel()
{
OpenDocuments = new ObservableCollection<DocumentViewModel>();
// Whenever the list of documents change, calculate a new Observable
// to represent whenever any of the *current* documents have been
// requested to close, then Switch to that. When we get something
// to close, remove it from the list.
OpenDocuments
.ToObservableChangeSet()
.AutoRefreshOnObservable(document => document.Close)
.Select(_ => WhenAnyDocumentClosed())
.Switch()
.Subscribe(x => OpenDocuments.Remove(x));
}
IObservable<DocumentViewModel> WhenAnyDocumentClosed()
{
// Select the documents into a list of Observables
// who return the Document to close when signaled,
// then flatten them all together.
return OpenDocuments
.Select(x => x.Close.Select(_ => x))
.Merge();
}
}
class Program
{
static void Main(string[] args)
{
var mainViewModel = new MainViewModel();
mainViewModel.OpenDocuments.Add(new DocumentViewModel());
mainViewModel.OpenDocuments.Add(new DocumentViewModel());
mainViewModel.OpenDocuments.Add(new DocumentViewModel());
mainViewModel.OpenDocuments.Add(new DocumentViewModel());
mainViewModel.OpenDocuments.First().Close.Execute().Subscribe();
Console.WriteLine("Hello World!");
}
}