当前位置: 首页 > 工具软件 > Messenger > 使用案例 >

MVVM中的Messenger

柯甫
2023-12-01

在开发Wpf/SL应用时,经常会遇到不同页面和窗体之间的参数传递的问题。对于这类问题,我们一般通过事件实现数据传递,也可以定义全局静态变量来进行数据共享。这里我们则使用了另外一种非常高效而优雅的方法来进行消息传递,这里我称之为Messenger,事实上,Messenger并非mvvm的专利,我们可以把它看作一种设计模式,你可以在其它.net程序中使用它。

 

一、Mvvm Light Messenger是什么

 

通过Mvvm Light源码我们可以知道Messenger的实现细节,如果你现在还不能理解这些代码也没关系,很多东西理解起来远比使用起来难,Messenger也是如此,它使用起来很简单,由于Messenger只公开了一些消息注册和发送方法,使用者一看便知方法的功能,而只需关注要发送的数据和接收的对象就可以了。

发送: 

[c-sharp] view plain copy print ?
  1. Messenger.Default.Send<bool?>(true);  

接收:

[c-sharp] view plain copy print ?
  1. Messenger.Default.Register<bool?>(this, m => this.DialogResult = m);  

这是最基本的用法,发送方发送了一个bool?类型的对象(值为true),这样任何只要注册了bool?类型消息的地方都可以接收到这个消息。

  • Send泛型方法很好理解,只是发送一个值为true的bool?类型的对象;
  • Register泛型方法接受2个参数,第一个是接受者,也就是消息的载体,通常是对象本身(this),当然也可以是其他已实例化的对象,第二个参数是Action类型的对象,是接收到消息后执行的方法委托

Register方法实际上将对象和Action方法添加到全局的字典集合当中,只不过他们关系是弱引用的关系,在Send方法获取对象引用,同时执行Action方法,有关弱引用的介绍,参考弱引用

 

Messenger通过全局的字典集合来保存弱引用关系,因此在对象不使用时,我们要养成清理的习惯,调用Unregister来从字典集合中移除引用关系。

[c-sharp] view plain copy print ?
  1. Messenger.Default.Unregister(this);  

 

二、应用示例

 

下面我们会通过登录界面实现和简单的列表增删改的功能来演示Messenger的用法:

1、登录部分:

首先创建LoginViewModel,类定义如下:

WPF:

[c-sharp] view plain copy print ?
  1. #region ICommand  
  2.   
  3.       public RelayCommand<object> LoginCommand  
  4.       {  
  5.           get   
  6.           {  
  7.               return new RelayCommand<object>(  
  8.                   (p) =>   
  9.                   {  
  10.                       System.Windows.Controls.PasswordBox pb = p as System.Windows.Controls.PasswordBox;  
  11.   
  12.                       bool isLogon = false;  
  13.   
  14.                       // 登录成功  
  15.                       if (_userName == "admin" && pb.Password == "123")  
  16.                           isLogon = true;  
  17.                       else  
  18.                           isLogon = false;  
  19.                         
  20.                       // 发送消息  
  21.                       Messenger.Default.Send<bool?>(isLogon);   
  22.                   }  
  23.                   );   
  24.           }  
  25.       }  
  26.   
  27.       public RelayCommand CancelCommand  
  28.       {  
  29.           get  
  30.           {  
  31.               return new RelayCommand(  
  32.                   () =>   
  33.                   {  
  34.                       // 发送消息  
  35.                       Messenger.Default.Send<bool?>(null);   
  36.                   }  
  37.                   );  
  38.           }  
  39.       }  
  40.        
  41.       #endregion  
  42.  
  43.       #region 公共属性  
  44.       public const string UserNamePropertyName = "UserName";  
  45.   
  46.       private string _userName = "";  
  47.   
  48.       public string UserName  
  49.       {  
  50.           get  
  51.           {  
  52.               return _userName;  
  53.           }  
  54.           set  
  55.           {  
  56.               if (_userName == value)  
  57.               {  
  58.                   return;  
  59.               }  
  60.   
  61.               _userName = value;  
  62.   
  63.               // Update bindings, no broadcast  
  64.               RaisePropertyChanged(UserNamePropertyName);  
  65.           }  
  66.       }  
  67.       #endregion  

SL:

[c-sharp] view plain copy print ?
  1. #region ICommand  
  2.   
  3.  public RelayCommand<string> LoginCommand  
  4.  {  
  5.      get   
  6.      {  
  7.          return new RelayCommand<string>(  
  8.              (p) =>   
  9.              {  
  10.                  bool isLogon = false;  
  11.   
  12.                  // 登录成功  
  13.                  if (_userName == "admin" && p == "123")  
  14.                      isLogon = true;  
  15.                  else  
  16.                      isLogon = false;  
  17.                    
  18.                  // 发送消息  
  19.                  Messenger.Default.Send<bool>(isLogon);   
  20.              }  
  21.              );   
  22.      }  
  23.  }  
  24.   
  25.  public RelayCommand CancelCommand  
  26.  {  
  27.      get  
  28.      {  
  29.          return new RelayCommand(  
  30.              () =>   
  31.              {   
  32.                  System.Windows.Browser.HtmlPage.Window.Invoke("close");   
  33.              }  
  34.              );  
  35.      }  
  36.  }  
  37.   
  38.  #endregion  
  39.  
  40.  #region 公共属性  
  41.  public const string UserNamePropertyName = "UserName";  
  42.   
  43.  private string _userName = "";  
  44.   
  45.  public string UserName  
  46.  {  
  47.      get  
  48.      {  
  49.          return _userName;  
  50.      }  
  51.      set  
  52.      {  
  53.          if (_userName == value)  
  54.          {  
  55.              return;  
  56.          }  
  57.   
  58.          _userName = value;  
  59.   
  60.          // Update bindings, no broadcast  
  61.          RaisePropertyChanged(UserNamePropertyName);  
  62.      }  
  63.  }  
  64.  #endregion  

接着创建Login窗体,将按钮命令绑定到LoginViewModel对应的Command,注意WPF中不能绑定PasswordBox的Password属性,因此我们将PasswordBox作为参数传递给LoginViewModel,这种写法不符合mvvm的思想,不过基本只有这里需要这么写,也无伤大雅,页面代码如下:

WPF:

  1. <StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal"   
  2.             HorizontalAlignment="Center">  
  3.   <TextBlock Text="用户名:" VerticalAlignment="Center"/>  
  4.   <TextBox Text="{Binding UserName,Mode=TwoWay}"  
  5.          Width="150" VerticalAlignment="Center"  
  6.          Margin="5,2,5,2"/>  
  7. </StackPanel>  
  8.   
  9. <StackPanel Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal"   
  10.             HorizontalAlignment="Center">  
  11.   <TextBlock Text="密  码:" VerticalAlignment="Center"/>  
  12.   <PasswordBox x:Name="password" Width="150" VerticalAlignment="Center"   
  13.                Margin="5,2,5,2" PasswordChar="*" />  
  14. </StackPanel>  
  15. <StackPanel Grid.Row="3" Grid.ColumnSpan="2"   
  16.             Orientation="Horizontal"   
  17.             HorizontalAlignment="Center" VerticalAlignment="Center">  
  18.   <Button Content="登录" Command="{Binding LoginCommand}"   
  19.           CommandParameter="{Binding ElementName=password}"   
  20.           Width="100" Margin="5,2,5,2"/>  
  21.   <Button Content="取消" Command="{Binding CancelCommand}"   
  22.           Width="100" Margin="5,2,5,2"/>  
  23. </StackPanel>  

SL中只有传递参数不一样:

  1. <Button Content="登录" Command="{Binding LoginCommand}"   
  2.         CommandParameter="{Binding Password,ElementName=password}"   
  3.         Width="100" Margin="5,2,5,2"/>  

 

最后在app.xaml.cs中添加登录逻辑

WPF(需要在xaml中去除StartupUri):

[c-sharp] view plain copy print ?
  1. protected override void OnStartup(StartupEventArgs e)  
  2. {  
  3.     base.OnStartup(e);  
  4.   
  5.     // 首先显示登录控件  
  6.     Login login = new Login();  
  7.     LoginViewModel loginViewModel = new LoginViewModel();  
  8.     login.DataContext = loginViewModel;  
  9.   
  10.     // 将Login设置为主窗体  
  11.     this.MainWindow = login;  
  12.     MainWindow.Show();  
  13.   
  14.     // 注册消息,接收bool类型的参数,true为登录成功  
  15.     Messenger.Default.Register<bool?>(  
  16.         this,  
  17.         m =>  
  18.         {  
  19.             // 登录成功后,显示主页面  
  20.             if (m.HasValue && m.Value)  
  21.             {  
  22.                 // 更改主窗体  
  23.                 this.MainWindow = new MainWindow();  
  24.   
  25.                 // 关闭登录窗体  
  26.                 login.Close();  
  27.                 // 清理释放Login资源  
  28.                 loginViewModel.Cleanup();  
  29.                 login = null;  
  30.   
  31.                 MainWindow.Show();  
  32.             }  
  33.             else if (!m.HasValue)  
  34.             {  
  35.                 MainWindow.Close();  
  36.             }  
  37.         }  
  38.         );  
  39. }  
  40.   
  41. protected override void OnExit(ExitEventArgs e)  
  42. {  
  43.     Messenger.Default.Unregister(this);  
  44.     base.OnExit(e);  
  45. }  

SL:

[c-sharp] view plain copy print ?
  1. private void ApplicationStartup(object sender, StartupEventArgs e)  
  2. {  
  3.     Grid rootvisual = new Grid();  
  4.   
  5.     // 首先显示登录控件  
  6.      Login login = new Login();  
  7.     LoginViewModel loginViewModel = new LoginViewModel();  
  8.     login.DataContext = loginViewModel;  
  9.   
  10.     rootvisual.Children.Add(login);  
  11.     RootVisual = rootvisual;  
  12.   
  13.     // 注册消息,接收bool类型的参数,true为登录成功  
  14.     Messenger.Default.Register<bool>(  
  15.         this,   
  16.         m =>   
  17.         {  
  18.             // 登录成功后,显示主页面  
  19.             if (m)  
  20.             {  
  21.                 // 移除登录控件  
  22.                 rootvisual.Children.Clear();  
  23.                 // 添加主页面  
  24.                 rootvisual.Children.Add(new MainPage());  
  25.                 // 清理释放Login资源  
  26.                 loginViewModel.Cleanup();  
  27.                 login = null;  
  28.             }  
  29.         }  
  30.         );  
  31.       
  32.     DispatcherHelper.Initialize();  
  33. }  
  34.   
  35. private static void ApplicationExit(object sender, EventArgs e)  
  36. {  
  37.     Messenger.Default.Unregister(sender);  
  38.     ViewModelLocator.Cleanup();  
  39. }  

 

到这里登录功能就实现了,关键地方就是在添加登录逻辑的地方,通过匿名方法和Lamda表达式,注册一个消息的执行方法就像写方法代码一样简单,只不过消息里的方法要等到send命令发送后才会执行

 

2、通过ChildWindow实现列表增删改

SL中模式对话框通过ChildWindow来实现,WPF通过Window的ShowDialog方法实现,这里我通过模拟SL的ChildWindow来实现WPF的模式对话框,有关如何在WPF中模拟SL的ChildWindow,参考:在WPF中模拟SL的ChildWindow效果

代码比较多,这里就不贴代码了,我的示例代码中都有详细的注释,下面主要说说一些需要关键的地方,也算是我的一些心得:

 

首先是Messenger的一些方法重载:

void Send<TMessage>(TMessage message);

发送值为message的TMessage类型的消息

 

void Send<TMessage, TTarget>(TMessage message);

 

发送值为message的TMessage类型的消息,但是接收对象必须是TTarget类型的对象

 

public virtual void Send<TMessage>(TMessage message, object token)

发送值为message的TMessage类型的消息,与前面不同的是接收对象注册的消息方法拥有相同的token值才能接收到消息值

 

void Register<TMessage>(object recipient, Action<TMessage> action);

 

注册接收TMessage类型消息的方法,recipient是消息载体,也就是接收消息的对象,action是消息执行方法的委托,该委托接受TMessage类型的参数,也就是Send发送的值

 

void Register<TMessage>(object recipient, bool receiveDerivedMessagesToo, Action<TMessage> action);

注册接收TMessage类型消息的方法,与上面不同的是receiveDerivedMessagesToo指定是否能够接收TMessage派生类型的对象作为消息的值

 

public virtual void Register<TMessage>(object recipient, object token, Action<TMessage> action)

注册接收TMessage类型消息的方法,与前面不同的是必须与Send方法相匹配的token才能接收该Send的消息值

 

public virtual void Register<TMessage>(object recipient, object token, bool receiveDerivedMessagesToo,Action<TMessage> action)

 

MvvmLight中还封装了一种特殊的消息类型NotificationMessageAction<TMessage>,通过它可以发送一些复杂的对象,并且可以包含回调函数,此示例中在对话框中发送确定的消息给主界面,主界面调用子对象的方法执行数据库操作,如果成功则关闭对话框,如果失败则执行回调函数,将错误信息返回给对话框并显示出来

 

最后需要主要注意的就是什么使用Messenger比较合适,例如在此示例中:

 

与弹出的对话框进行交互,我会将主界面作为消息的载体,原因如下:

弹出对话框的生命周期较短,因此将弹出对话框作为发送方,总能发送到它的宿主页面

弹出对话框一般需要重复打开,在弹出对话框中注册消息方法会增加消息清理的成本,即在每次关闭对话框时要对消息进行清理,否则每打开一次对话框,消息执行次数会递增

 

 

本章节示例代码下载地址:示例下载 

 类似资料: