【Prism系列】Prism事件聚合器

命令和事件        

我们知道MVVM指的就是Model ViewModel 与 View,之前为了做到业务逻辑和前台彻底分离,我们使用了Command命令,将界面的的输入或者是某些控件的事件,转化为命令。这样业务逻辑就从View搬到了ViewModel。很明显这个传递方向是View -》ViewModel。

        但是现在有个一个需求是需要ViewModel通知View某个事情,比如ViewModel告诉View你需要弹出一个窗口!那这个时候,我们通常在View中订阅一个事件,需要通知时在在ViewModel中发布一个事件,这样View就能收到ViewModel的消息了。此时我们的事件聚合器就登场了。

获取事件聚合器对象

我们先看一张图:

 这些是Prism框架帮我们预先在容器里注册的内容,其中第11个就是我们今天的主角儿,事件聚合器,既然是注册好了,我们就可以通过构造函数注入,拿到这个对象。

 //[Unity.Dependency]
 IEventAggregator eventAggregator;

 public PrismEventPageViewModel(IEventAggregator eventAggregator)
 {
     this.eventAggregator = eventAggregator;
 }

这里我试了一下,只能通过构造函数去拿这个对象,无法通过自动注入属性获取。

事件聚合器也是一个容器,拿到了事件事件聚合器的对象之后,就可以通过它获取事件对象了,

这个对象,需要我们先准备一个事件类:

internal class MyPrismEvent:PubSubEvent
{
}

internal class MyPrismEvent2 : PubSubEvent<object>
{
}

这里,我准备了两个类,都继承自PubSubEvent,一个表示不带参数,一个表示带一个参数,类型为object。

摘取,订阅,发布的关键代码:

//订阅部分
eventAggregator.GetEvent<MyPrismEvent>().Subscribe(doMyPrismEvent, ThreadOption.BackgroundThread);
eventAggregator.GetEvent<MyPrismEvent2>().Subscribe(doMyPrismEvent2);

//发布部分
eventAggregator.GetEvent<MyPrismEvent2>().Publish("123456789");
eventAggregator.GetEvent<MyPrismEvent>().Publish();

完整代码

我们在View类中进行订阅:


    /// <summary>
    /// Prism事件聚合器.xaml 的交互逻辑
    /// </summary>
    public partial class PrismEventPage : Page
    {
        //[Unity.Dependency]
        IEventAggregator eventAggregator;
        public PrismEventPage(IEventAggregator eventAggregator)
        {
            InitializeComponent();
            this.eventAggregator = eventAggregator;
        }
        private void Page_Loaded(object sender, RoutedEventArgs e)
        {         
            eventAggregator.GetEvent<MyPrismEvent>().Subscribe(doMyPrismEvent, ThreadOption.BackgroundThread);
            eventAggregator.GetEvent<MyPrismEvent2>().Subscribe(doMyPrismEvent2);
        }
        private void Page_Unloaded(object sender, RoutedEventArgs e)
        {
            //这里需要卸载,如果Subscribe多次,回调也会执行多次
            eventAggregator.GetEvent<MyPrismEvent>().Unsubscribe(doMyPrismEvent);
            eventAggregator.GetEvent<MyPrismEvent2>().Unsubscribe(doMyPrismEvent2);
        }
        void doMyPrismEvent()
        {
            System.Diagnostics.Debug.WriteLine($"111111Event Action ID:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
            MessageBox.Show("弹出窗口");           
        }
        void doMyPrismEvent2(object obj)
        {
            System.Diagnostics.Debug.WriteLine($"Event Action ID:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
            MessageBox.Show($"弹出窗口:{obj}");
        }    
    }

这里注意几个点:

1 同样使用构造注入拿到事件聚合器的对象,这里的对象和在ViewMode中拿到的是相同的对象。

2 Page加载时订阅,Page卸载时取消订阅,不然Page多次加载会导致多次订阅,从而导致回调多次执行.

3 订阅事件时,可以通过ThreadOption这个枚举指定回调是执行在UI线程还是其他线程。

因为发布时的代码会在UI线程或其他线程。如果发布代码和回调都在UI,那他们之间会有一个先后关系,及发布代码在调用了Publish之后会等待回调完成,再执行Publish之后的代码。反之一个再UI线程一个再其他线程,就不存在这样的先后关系,具体可以更具我提供的代码进行测试。

我们在ViewModel类中进行发布:

    internal class PrismEventPageViewModel
    {
        //[Unity.Dependency]
        IEventAggregator eventAggregator;
        public int MyProperty { get; set; } = 999;
        public string Hello { get; set; } = "PrismEventViewModel";
        public DelegateCommand BtnCommand { get; set; }
        public DelegateCommand BtnCommand2 { get; set; }
        public PrismEventPageViewModel(IEventAggregator eventAggregator)
        {
            this.eventAggregator = eventAggregator;
            BtnCommand = new DelegateCommand(Excute);
            BtnCommand2 = new DelegateCommand(Excute2);
        }
        void Excute()
        {
            System.Diagnostics.Debug.WriteLine($"Excute!!!!! Action ID:{System.Threading.Thread.CurrentThread.ManagedThreadId}");
            eventAggregator.GetEvent<MyPrismEvent>().Publish();
            Task.Delay(3000).Wait();
            System.Diagnostics.Debug.WriteLine("Publish完成");

        }
        void Excute2()
        {
            eventAggregator.GetEvent<MyPrismEvent2>().Publish("123456789");
        }
    }

View和ViewModel的自动匹配

最后还有一个匹配的问题需要注意。在Page里必须加上这两句,之前测试Windows是不用的,但是Page这里必须使用(测试环境是vs2022 .net6.0)

xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"

2022年10月12日,最近测试,发现用用户控件作为子窗口的时候,也必须加上这两句,不然会导致bingding不上,所以这两句还是挺重要的,建议任何时候都加上!

我手动匹配都没起作用:

//不知道为啥对Page不起作用~~~
ViewModelLocationProvider.Register<PrismEventPage, PrismEventPageViewModel>();

但是加上上面两句就OK了。


聚合器的妙用(LoadingRow --> LoadedRow

在实际的项目中,我遇到一个问题,就是DataGrid有一个事件叫做LoadingRow, 但是这个事件触发的时机是在添加一行之前,所以在这个事件里面无法获取刚刚添加的这一行的所有Cell。

但是DataGrid有没有提供LoadedRow这种事件。这里我们可以通过事件聚合器解决这个问题:

首先为添加DataGrid事件:

<DataGrid Grid.Row="2" x:Name="testDateGrid">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="LoadingRow">
            <prism:InvokeCommandAction Command="{Binding EventLoadingRow}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</DataGrid>

然后这么写:

 LoadingRow触发一个事件,这个事件订阅的时候,要使用UI线程,这样的话,就相当于构建了一个LoadedRow事件!

public class WaitEvent : PubSubEvent
{
}

//-----------------------------------------


//行增加事件
EventLoadingRow = new DelegateCommand<DataGridRowEventArgs>((e) => {
    _eventAggregator.GetEvent<WaitEvent>().Publish();
});

_eventAggregator.GetEvent<WaitEvent>().Subscribe(() => {
                var rowContainer = testDateGrid.GetRow(0);
                //var cell = testDateGrid.GetCell(row, 0);

                if (rowContainer != null)
                {
                    DataGridCellsPresenter presenter = Tool.GetVisualChild<DataGridCellsPresenter>(rowContainer);
                    if (presenter == null) return;
                    DataGridCell cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(0);
                    if (cell == null)
                    {
                        testDateGrid.ScrollIntoView(rowContainer, testDateGrid.Columns[0]);
                        cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(0);
                    }
                    if (cell != null)
                    {
                        TextBlock tb = cell.Content as TextBlock;
                        Console.WriteLine(tb.Text);
                        // dgSourceData.ScrollIntoView(row, dgSourceData.Columns[colindex]);
                        //cell = (DataGridCell)presenter.ItemContainerGenerator.ContainerFromIndex(colindex);
                        cell.Focus();
                        cell.Background = new SolidColorBrush(Colors.Red);//OK!问题解决,选中单元格变色
                        cell.Foreground = new SolidColorBrush(Colors.Yellow);
                        cell.FontSize = 20;
                    }

                }
            }, ThreadOption.UIThread);

这段代码的目的就是为了获取行中的单元格,并且修改单元格的颜色。

posted @ 2022-08-23 19:25  宋桓公  阅读(539)  评论(0)    收藏  举报