Prism教程五: EventAggregation模块间的通信

来源:互联网 发布:sql循环语句 update 编辑:程序博客网 时间:2024/05/21 17:04

在开发Silverlight程序的时候,经常需要在不同的组件间进行通讯。比如点击一个button,可能就需要改变另一个控件的内容。比较直接的办法是使用时间,当然使用MVVM的时候也可以使用Command,还可以定义一些全局变量来保存一些信息等。

Prism提供了几种用于组件间通信的途径,可以使用RegionContext使不同的视图共享数据,也可以借助于容器的力量来使用共享的service来进行通信,或者使用command等。除此之外,Prism还提供了一种基于事件的多播发布/订阅方式的通信机制,使不同的组件之前能够以一种松散耦合的方式来进行通信。这就是本文要介绍的事件聚合(Event Aggregation)。

事件聚合的过程有点像收听广播,首先要有个固定的频率,然后内容就会在这个频率上广播出去,至于有没有人收听,广播电台是不知道的,他只是把内容播送出去。而其他的人想听广播也不用跑到广播电台,只要知道频率,收到这个频率就可以了。联系广播电台和听众的就是这个频率。

在事件聚合的过程中,事件发布方(Publisher)相当于广播电台,事件接收方(Subscriber)相当于听众,而事件自然就相当于频率了。

使用Event Aggregation很简单,只需要知道一个接口和一个类基本上就足够了。接口是IEventAggregation,类是CompositePresentationEvent。

要想发布或者订阅事件,自然需要先有事件,所以第一件工作就是要定义事件。Prim提供了一个事件基类CompositePresentationEvent<TPayload>,自定义的事件只需要继承这个类就可以了,泛型代表的是事件发生过程中需要传递的参数类型。如:

public class ReceiveNewEmailEvent : CompositePresentationEvent<MailViewModel>{}


 

上面定义了一个事件,用于在收到新邮件时使用,传递的参数是一个邮件的ViewModel。

使用的时候也很简单,使用IEventAggregation接口中的GetEvent<TEventType>方法来获取事件,然后要么发布出去,要么订阅一下就可以了。

下面是当收到一封新的邮件的时候,发布事件的代码:

public class EmailReceiver{    private IEventAggregator _eventAggregator;    public EmailReceiver(IEventAggregator eventAggregator)    {        _eventAggregator = eventAggregator;    }     public void ReceiveEmail()    {        if (_email != null)        {   //  当接收到新邮件时,就发布事件,所有订阅了该事件的组件都会接到通知            _eventAggregator.GetEvent<ReceiveNewEmailEvent>()                .Publish(_email);        }    }}


可以看到我们直接在构造函数中传递了IEventAggregation类型的参数,如果使用Prism来搭建Silverlight程序的话,那么在默认的Bootstrapper中会在容器中添加IEventAggregator的实例,所以并不需要我们做其他更多的工作。

下面是订阅ReceiveNewEmail事件的代码:

public class MailBox{    public MailBox(IEventAggregator eventAggregator)    {        eventAggregator.GetEvent<ReceiveNewEmailEvent>()            .Subscribe(OnReceivedNewEmail);    }     //  该方法必须为public    public void OnReceivedNewEmail(MailViewModel mail)    {        //  do something    }}


这样,发布出去的事件马上就可以被接收到,而且两个组件只是依赖于事件,彼此之间是松耦合的。

事件可以订阅,也可以退订,甚至可以有选择地接受某些特定的事件。下面以一个模拟的简单的邮箱客户端来演示一下Event Aggregation的使用场景。

如图所示,左边的四邮件列表,会有一个定时器每隔两秒钟接收到一封邮件,这时邮箱客户端会更新邮件列表,点击左边的列表,会在右边显示邮件的内容。如果点击“将该发信人加入黑名单”,则不会接收来自该发件人的邮件,如果点击断开连接,则停止接收邮件,再次点击会继续接收邮件。

首先在启动程序的时候开启一个定时器,每个两秒钟就会接收一封邮件,并发布事件通知有新邮件:

public class EmailReceiver{    public void Run()    {        var timer = new DispatcherTimer();        timer.Tick += (s, e) => EventAggregatorRepository.EventAggregator                                    .GetEvent<ReceiveNewEmailEvent>()                                    .Publish(EmailRepository.GetMail());        timer.Interval = new TimeSpan(0, 0, 0, 2);        timer.Start();    } }


 

MailList组件会订阅这个事件,并对邮件列表进行更新:

public partial class MailList : UserControl{    private readonly ObservableCollection<MailViewModel> _mails =         new ObservableCollection<MailViewModel>();     //  黑名单列表    private readonly List<string> _refusedSenders = new List<string>();             public MailList()    {        InitializeComponent();         MailItems.ItemsSource = _mails;         SubscribeReceiveEmailEvent();    }     private void SubscribeReceiveEmailEvent()    {   //  订阅事件的Subscribe方法提供了几个重载方法,除了最简单的直接订阅之外,        //  还可以指定线程类型(比如如果直接使用System.Threading.Timer的话,        //  就必须使用ThreadOption.UIThread,否则会报错),以及是否持有订阅者的引用,        //  或者指定一个filter来对事件进行过滤        //  本例中使用的filter是拒绝接受黑名单中包含的发件人发过来的邮件        EventAggregatorRepository.EventAggregator            .GetEvent<ReceiveNewEmailEvent>()            .Subscribe(OnReceiveNewEmail, ThreadOption.UIThread,            true, (mail) => !_refusedSenders.Contains(mail.From));    }     public void OnReceiveNewEmail(MailViewModel mail)    {        _mails.Insert(0, mail);    }}


当点击左边的邮件列表的时候,会在右边的MailContext组件中显示该邮件的信息,这个过程也是通过Event Aggregation来完成的。

//  NotificationObject是Prism提供的对MVVM的支持的ViewModel的基类//  可以简化INotifyPropertyChanged接口的实现方式public class MailViewModel : NotificationObject{    public MailViewModel()    {   //  DelegateCommand也是Prism提供的一种Command类型        ViewMailCommand = new DelegateCommand(OnViewMail);    }             public ICommand ViewMailCommand { get; private set; }     public void OnViewMail()    {        this.HasRead = true;        EventAggregatorRepository.EventAggregator            .GetEvent<ViewEmailEvent>()            .Publish(this);    }}


当点击时,会进入相应的Command逻辑,而MailContent则订阅了ViewEmailEvent,并将传递过来的MailViewModel显示出来:

public partial class MailContent : UserControl{    public MailContent()    {        InitializeComponent();         EventAggregatorRepository.EventAggregator            .GetEvent<ViewEmailEvent>()            .Subscribe(OnViewEmail);    }     public void OnViewEmail(MailViewModel mail)    {        this.DataContext = mail;    }}


当点击将发信人加入黑名单按钮时,会发布AddRefuseSenderEvent,而接收到这一事件的MailList组件则会更新黑名单,这样filter就会过滤掉黑名单中已经存在的发件人的邮件:

public void OnRefusedSendersAdded(string sender){    if (!_refusedSenders.Contains(sender))    {        _refusedSenders.Add(sender);    }}


如果点击了断开连接或重新连接的话,会发布一个ConnectionDisconnectMailServerEvent事件。Prism的事件基类并不支持不带参数的事件,也就是说没有办法创建一个不需要传参的事件。所以这里我们使用了object类型作为参数类型,在传递参数的时候直接传了个null过去。

EventAggregatorRepository.EventAggregator    .GetEvent<ConnectOrDisconnectMailServerEvent>()    .Publish(null);


而当MailList接收到该事件的时候,首先判断一下是否已经订阅了ReceiveNewEmailEvent事件,如果订阅了就退订,如果没有订阅就重新订阅。这样来达到开启或关闭接收邮件的目的。

public partial class MailList : UserControl{    private readonly ObservableCollection<MailViewModel> _mails =         new ObservableCollection<MailViewModel>();     private readonly List<string> _refusedSenders = new List<string>();             public MailList()    {        InitializeComponent();         SubscribeReceiveEmailEvent();         EventAggregatorRepository.EventAggregator            .GetEvent<ConnectOrDisconnectMailServerEvent>()            .Subscribe(OnConnectOrDisconnectMailServer);    }     public void OnConnectOrDisconnectMailServer(object obj)    {        //  判断是否已经订阅了该事件        bool hasSubscribed = EventAggregatorRepository.EventAggregator            .GetEvent<ReceiveNewEmailEvent>()            .Contains(OnReceiveNewEmail);        if (hasSubscribed)        {            UnsubscribeReceiveEmailEvent();        }        else        {            SubscribeReceiveEmailEvent();        }    }     private void SubscribeReceiveEmailEvent()    {        EventAggregatorRepository.EventAggregator            .GetEvent<ReceiveNewEmailEvent>()            .Subscribe(OnReceiveNewEmail, ThreadOption.UIThread,            true, (mail) => !_refusedSenders.Contains(mail.From));    }     private void UnsubscribeReceiveEmailEvent()    {   //  退订事件        EventAggregatorRepository.EventAggregator            .GetEvent<ReceiveNewEmailEvent>()            .Unsubscribe(OnReceiveNewEmail);    }     public void OnReceiveNewEmail(MailViewModel mail)    {        _mails.Insert(0, mail);    }}


由于EventAggregation并不需要建立在Prism装配的程序上,为了操作简便,所以并没有使用Prism来管理这个程序,当然也就没有使用容器。所以这里使用了一个Static的全局变量来保存一个IEventAggregation的实例。

本文为了演示,使用了大量的Event Aggregation,希望大家在工作中仔细斟酌是哟个,虽然用起来很灵活,但是如果事件太多,也会让人无从下手的感觉,增加维护的难度。

 源代码:  http://download.csdn.net/detail/eric_k1m/6223539

下面详细说一下该Demo的创建过程:

1. 新建一个Silverlight Application——EventAggregatorDemo,新建一个UserControl用作Page最外框布局使用的MailBox使用。

<UserControl x:Class="EventAggregatorDemo.MailBox"    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"             xmlns:local="clr-namespace:EventAggregatorDemo"    mc:Ignorable="d"    d:DesignHeight="300" d:DesignWidth="400">        <Grid x:Name="LayoutRoot" Background="White" ShowGridLines="True">        <Grid.ColumnDefinitions>            <ColumnDefinition Width="200"/>            <ColumnDefinition Width="*"/>        </Grid.ColumnDefinitions>                <Border BorderBrush="Gray" BorderThickness="2">            <StackPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch">                <TextBlock Text="所有邮件:"/>                <local:MailList/>            </StackPanel>        </Border>                <local:MailContent Grid.Column="1"/>    </Grid></UserControl>


 

2. 在MailBox中我们用到了两个子页面,先新建其中一个MailList的UserControl:

<UserControl x:Class="EventAggregatorDemo.MailList"    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"    xmlns:local="clr-namespace:EventAggregatorDemo.Main"    mc:Ignorable="d"    d:DesignHeight="300" d:DesignWidth="400">        <UserControl.Resources>        <ResourceDictionary>            <local:HasReadToFontWeightConverter x:Key="hasReadToFontWeightConverter" />                                <Style x:Key="RadioButtonStyle" TargetType="ToggleButton">                <Setter Property="Background" Value="{x:Null}"/>                <Setter Property="Foreground" Value="#FF0E233E"/>                <Setter Property="Padding" Value="3"/>                <Setter Property="BorderThickness" Value="1"/>                <Setter Property="FontSize" Value="12"/>                <Setter Property="BorderBrush" Value="#FF204d89" />                <Setter Property="Template">                    <Setter.Value>                        <ControlTemplate TargetType="ToggleButton">                            <Grid>                                <VisualStateManager.VisualStateGroups>                                    <VisualStateGroup x:Name="CommonStates">                                        <VisualState x:Name="Normal"/>                                        <VisualState x:Name="MouseOver">                                            <Storyboard>                                                <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="MouseOverBackground" d:IsOptimized="True"/>                                            </Storyboard>                                        </VisualState>                                        <VisualState x:Name="Pressed">                                            <Storyboard>                                                <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="PressedBackground" d:IsOptimized="True"/>                                            </Storyboard>                                        </VisualState>                                        <VisualState x:Name="Disabled">                                            <Storyboard>                                                <DoubleAnimation Duration="0" To=".55" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DisabledVisualElement"/>                                            </Storyboard>                                        </VisualState>                                    </VisualStateGroup>                                    <VisualStateGroup x:Name="FocusStates">                                        <VisualState x:Name="Focused">                                            <Storyboard>                                                <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="FocusVisualElement"/>                                            </Storyboard>                                        </VisualState>                                        <VisualState x:Name="Unfocused"/>                                    </VisualStateGroup>                                    <VisualStateGroup x:Name="CheckStates">                                        <VisualState x:Name="Indeterminate"/>                                        <VisualState x:Name="Checked">                                            <Storyboard>                                                <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="CheckedBackground" d:IsOptimized="True"/>                                            </Storyboard>                                        </VisualState>                                        <VisualState x:Name="Unchecked"/>                                    </VisualStateGroup>                                </VisualStateManager.VisualStateGroups>                                <Border x:Name="BaseBackground" BorderBrush="#FFD1E5FF" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3">                                    <Border.Background>                                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">                                            <GradientStop Color="#FFD1E4FF" Offset="0"/>                                            <GradientStop Color="#FFC0D8FF" Offset="1"/>                                            <GradientStop Color="#FFADD1FF" Offset="0.301"/>                                            <GradientStop Color="#FFD1E5FF" Offset="0.3"/>                                        </LinearGradientBrush>                                    </Border.Background>                                </Border>                                <Border x:Name="MouseOverBackground" BorderBrush="#FFFFE8A7" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" Opacity="0">                                    <Border.Background>                                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">                                            <GradientStop Color="White" Offset="0"/>                                            <GradientStop Color="#FFFFE69E" Offset="1"/>                                            <GradientStop Color="#FFFFE8A7" Offset="0.3"/>                                            <GradientStop Color="#FFFFD767" Offset="0.301"/>                                        </LinearGradientBrush>                                    </Border.Background>                                </Border>                                <Border x:Name="PressedBackground" BorderBrush="#FFFFAC42" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" Opacity="0">                                    <Border.Background>                                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">                                            <GradientStop Color="#FFFFDB69" Offset="0"/>                                            <GradientStop Color="#FFFEBA66" Offset="1"/>                                            <GradientStop Color="#FFFFAC42" Offset="0.3"/>                                            <GradientStop Color="#FFFB8E3E" Offset="0.301"/>                                        </LinearGradientBrush>                                    </Border.Background>                                </Border>                                <Border x:Name="CheckedBackground" BorderBrush="#FFFFBB6E" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" Opacity="0">                                    <Border.Background>                                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">                                            <GradientStop Color="#FFFFD9AA" Offset="0"/>                                            <GradientStop Color="#FFFEE17A" Offset="1"/>                                            <GradientStop Color="#FFFFBB6E" Offset="0.3"/>                                            <GradientStop Color="#FFFFAE42" Offset="0.301"/>                                        </LinearGradientBrush>                                    </Border.Background>                                </Border>                                <Grid Margin="{TemplateBinding BorderThickness}">                                    <ContentPresenter x:Name="ContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>                                </Grid>                                <Rectangle x:Name="DisabledVisualElement" Fill="#FFFFFFFF" IsHitTestVisible="false" Opacity="0" RadiusY="3" RadiusX="3"/>                                <Rectangle x:Name="FocusVisualElement" IsHitTestVisible="false" Margin="1" Opacity="0" RadiusY="2" RadiusX="2" Stroke="{x:Null}" StrokeThickness="1" />                            </Grid>                        </ControlTemplate>                    </Setter.Value>                </Setter>            </Style>        </ResourceDictionary>    </UserControl.Resources>        <Grid x:Name="LayoutRoot" Background="White">        <ItemsControl x:Name="MailItems">            <ItemsControl.ItemTemplate>                <DataTemplate>                    <StackPanel>                        <RadioButton Command="{Binding ViewMailCommand}" GroupName="EmailItem"                                     Style="{StaticResource RadioButtonStyle}" VerticalContentAlignment="Center" Height="35" Padding="15,0,0,0">                            <StackPanel>                                <TextBlock Text="{Binding From}" FontSize="10" Foreground="Gray" />                                <TextBlock Text="{Binding Subject}" FontWeight="{Binding HasRead, Converter={StaticResource hasReadToFontWeightConverter}}"  FontSize="14" />                            </StackPanel>                        </RadioButton>                        <Border BorderBrush="Silver" BorderThickness="1" />                    </StackPanel>                </DataTemplate>            </ItemsControl.ItemTemplate>        </ItemsControl>    </Grid></UserControl>


 3. 这里用到了一个Converter——hasReadToFontWeightConverter,新建一个类HasReadToFontWeightConverter,它实现了IValueConverter,用于数据转换工作,也就是把显示标题的TextBlock的FontWeight属性是否是粗体。

using System;using System.Net;using System.Windows;using System.Windows.Controls;using System.Windows.Documents;using System.Windows.Ink;using System.Windows.Input;using System.Windows.Media;using System.Windows.Media.Animation;using System.Windows.Shapes;using System.Windows.Data;namespace EventAggregatorDemo{    public class HasReadToFontWeightConverter:IValueConverter    {        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)        {            bool hasRead = (bool)value;            return hasRead ? FontWeights.Normal : FontWeights.Bold;        }        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)        {            throw new NotImplementedException();        }    }}


4. 新建一个类EventAggregatorRepository

using System;using System.Net;using System.Windows;using System.Windows.Controls;using System.Windows.Documents;using System.Windows.Ink;using System.Windows.Input;using System.Windows.Media;using System.Windows.Media.Animation;using System.Windows.Shapes;using Microsoft.Practices.Prism.Events;namespace EventAggregatorDemo{    public class EventAggregatorRepository    {        private static IEventAggregator _eventAggregator;        private static IEventAggregator EventAggregator        {            get            {                if (_eventAggregator == null)                {                    _eventAggregator = new EventAggregator();                }                return _eventAggregator;            }        }    }}