如何实现ViewModel间通信?(Prism事件聚合器可行吗?)
嘿,作为刚上手C#和WPF的新手就敢直接冲MVVM,这勇气必须给你点个赞!虽然把小应用搞复杂了,但确实是练手的绝佳机会——我当初学MVVM的时候,也在ViewModel之间的交互这儿卡了好久,太懂这种摸不着头脑的迷茫了😂
下面给你分享几种实用的解决方案,都是我踩坑后总结出来的,适合新手快速上手:
MVVM的核心就是解耦,所以绝对不能让两个ViewModel直接互相引用,不然就违背模式的初衷了。常用的方法都是通过一个中间“桥梁”来传递消息,下面分三种情况说:
1. 自己实现极简版“信使”(Messenger)模式
不用引入任何第三方框架,自己写几十行代码就能搞定,新手友好还能加深理解。
先写一个全局的Messenger类,负责消息的订阅和发送:
public static class Messenger { // 存储不同类型消息的订阅者 private static readonly Dictionary<Type, List<Action<object>>> _subscribers = new(); // 订阅指定类型的消息 public static void Subscribe<T>(Action<T> action) { var messageType = typeof(T); if (!_subscribers.ContainsKey(messageType)) { _subscribers[messageType] = new List<Action<object>>(); } _subscribers[messageType].Add(obj => action((T)obj)); } // 发送指定类型的消息 public static void Send<T>(T message) { var messageType = typeof(T); if (_subscribers.TryGetValue(messageType, out var actions)) { // 用ToList避免遍历过程中集合被修改 foreach (var action in actions.ToList()) { action(message); } } } // 取消订阅,一定要记得调用,防止内存泄漏 public static void Unsubscribe<T>(Action<T> action) { var messageType = typeof(T); if (_subscribers.TryGetValue(messageType, out var actions)) { actions.RemoveAll(a => a.Target == action.Target && a.Method == action.Method); if (actions.Count == 0) { _subscribers.Remove(messageType); } } } }
然后定义一个消息类,用来传递绘制矩形的参数:
public class DrawRectangleMessage { public double X { get; set; } public double Y { get; set; } public double Width { get; set; } public double Height { get; set; } public Brush Fill { get; set; } }
接下来在OverlayViewModel里订阅消息,收到消息后添加矩形数据:
public class OverlayViewModel : INotifyPropertyChanged { private ObservableCollection<RectangleModel> _rectangles; public ObservableCollection<RectangleModel> Rectangles { get => _rectangles; set { _rectangles = value; OnPropertyChanged(); } } public OverlayViewModel() { Rectangles = new ObservableCollection<RectangleModel>(); // 订阅绘制矩形的消息 Messenger.Subscribe<DrawRectangleMessage>(msg => { Rectangles.Add(new RectangleModel { X = msg.X, Y = msg.Y, Width = msg.Width, Height = msg.Height, Fill = msg.Fill }); }); } // 实现INotifyPropertyChanged接口,通知UI更新 public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // 建议在View的Unloaded事件里取消订阅,比析构函数更可靠 public void Cleanup() { Messenger.Unsubscribe<DrawRectangleMessage>(msg => { Rectangles.Add(new RectangleModel { X = msg.X, Y = msg.Y, Width = msg.Width, Height = msg.Height, Fill = msg.Fill }); }); } }
在MainWindowViewModel里,需要绘制矩形的时候就发送消息:
public class MainWindowViewModel : INotifyPropertyChanged { public ICommand DrawRectangleCommand { get; } public MainWindowViewModel() { DrawRectangleCommand = new RelayCommand(() => { // 这里可以替换成你实际获取的矩形参数 var drawMsg = new DrawRectangleMessage { X = 50, Y = 50, Width = 100, Height = 80, Fill = Brushes.LightBlue }; // 发送消息 Messenger.Send(drawMsg); }); } // 同样实现INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
顺便给你一个简单的RelayCommand实现(如果不用第三方框架的话):
public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; public event EventHandler CanExecuteChanged; public RelayCommand(Action execute, Func<bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) { return _canExecute?.Invoke() ?? true; } public void Execute(object parameter) { _execute(); } public void RaiseCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } }
最后在OverlayView里绑定数据,用Canvas和ItemsControl绘制矩形:
<Canvas> <ItemsControl ItemsSource="{Binding Rectangles}"> <ItemsControl.ItemTemplate> <DataTemplate> <Rectangle Width="{Binding Width}" Height="{Binding Height}" Fill="{Binding Fill}"> <Canvas.Left> <Binding Path="X"/> </Canvas.Left> <Canvas.Top> <Binding Path="Y"/> </Canvas.Top> </Rectangle> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Canvas>
2. 用第三方MVVM框架的现成工具
如果不想自己写Messenger,也可以用成熟的框架,省很多事:
- MVVM Light:自带
Messenger.Default,用法和上面自己写的差不多,还支持弱引用,更不容易内存泄漏。先安装NuGet包MvvmLight,然后发送消息用Messenger.Default.Send(...),订阅用Messenger.Default.Register<DrawRectangleMessage>(this, msg => { /* 处理逻辑 */ }),销毁时用Messenger.Default.Unregister(this)。 - Prism:适合复杂一点的应用,用
IEventAggregator来发布订阅事件,解耦性更强,不过学习成本稍微高一点。
3. 共享状态服务(超简单的小应用专属)
如果你的应用特别小,也可以搞一个单例的状态服务,两个ViewModel都引用它,通过服务的事件来传递消息:
public class DrawingStateService { // 单例模式,保证全局只有一个实例 public static DrawingStateService Instance => _instance ??= new DrawingStateService(); private static DrawingStateService _instance; // 定义事件,OverlayViewModel订阅这个事件 public event Action<DrawRectangleMessage> RectangleDrawRequested; // MainWindowViewModel调用这个方法触发事件 public void RequestDrawRectangle(DrawRectangleMessage msg) { RectangleDrawRequested?.Invoke(msg); } }
然后在MainWindowViewModel里调用DrawingStateService.Instance.RequestDrawRectangle(...),OverlayViewModel的构造函数里订阅DrawingStateService.Instance.RectangleDrawRequested += (msg) => { /* 添加矩形逻辑 */ }。这种方式最简单,但耦合度比信使模式高一点,适合小应用快速实现。
最后提醒两句
- 不管用哪种方式,一定要记得取消订阅/移除事件处理程序,不然ViewModel销毁不了会导致内存泄漏!
- 尽量让每个ViewModel只负责自己的逻辑,不要互相依赖,这样后续扩展起来才不会头疼。
内容的提问来源于stack exchange,提问作者Kyle

