65.9K
CodeProject 正在变化。 阅读更多。
Home

Prism for Silverlight/MEF 简易教程。第三部分 - 模块间的通信

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (47投票s)

2011年2月20日

CPOL

11分钟阅读

viewsIcon

156291

downloadIcon

3374

PRISM 教程的第三部分,描述模块之间的通信

引言

这是“Prism for Silverlight/MEF 简易教程”三部曲的第三部分(也是最后一部分)。它介绍了应用程序中不同模块之间的通信。

以下是指向 第一部分:Prism 模块第二部分:Prism 导航 的链接。

本教程部分假定您已具备 C#、MEF 和 Silverlight 的一些知识,以及 Prism 模块和区域的概念,这些概念可以从本教程的第一部分和第二部分中学习。

正如我们在 第一部分:Prism 模块 中所学到的,Prism 模块是可独立部署的软件单元(WPF 中的 *.dll 文件,Silverlight 中的 *.xap 文件)。用于组装其他模块的主模块称为“应用程序”。

不同模块之间的通信是一个挑战,因为大多数模块不直接引用彼此(独立性条件),因此无法直接访问彼此的功能。然而,模块可以引用其他项目,这些项目可以为它们提供访问通用通信数据和通信接口的方式。

Prism 模块之间有三种通信方式:

  1. 通过 Prism 服务:一个通用的 MEF 服务定义在一个项目中,该项目被所有使用它的模块引用用于通信。
  2. 通过 Prism 区域上下文:数据可以从包含区域的控件传输到加载到该区域的模块。
  3. 通过 Prism 的事件聚合器:这是模块间通信最强大、最简单的方法——与 Prism 服务不同,它不需要构建任何额外的服务;与区域上下文方法不同,它可以用于任何两个模块之间的通信,而不仅仅是区域层次结构中的模块。

模块间通信概述和示例

本教程包含 3 个示例——每个示例演示上述模块间通信的一种方式。在所有这些示例中,一个模块中的 `string` 被复制到另一个模块并在其中显示。

通过服务进行模块间通信

此项目的源代码可在“*CommunicationsViaAService.sln*”解决方案下找到。

两个模块:`Module1` 和 `Module2` 由引导程序加载到应用程序(主模块)中。应用程序和这两个模块依赖于一个非常轻量级的项目,名为“`Common`”,其中包含一个用于模块间通信服务的接口。

    public interface IStringCopyService
    {
        event Action<string> CopyStringEvent;

        void Copy(string str);
    }

服务的 MEF 可实现位于应用程序模块内。

    [Export(typeof(IStringCopyService))]
    public class StringCopyServiceImpl : IStringCopyService
    {
        #region IStringCopyService Members

        public event Action<string> CopyStringEvent;

        public void Copy(string str)
        {
            if (CopyStringEvent != null)
                CopyStringEvent(str);
        }

        #endregion
    }

请注意,由于我们没有设置 `PartCreationPolicy` 属性,`StringCopyServiceImpl` 将默认共享,即 `StringCopyServiceImpl` 对象在解决方案中将是单例。

此服务用于将一个字符串从 `Module1` 发送到 `Module2`。

`Module1View` MEF 导入对此服务的引用(参见 *Module1View.xaml.cs* 文件)。

        [Import]
        public IStringCopyService TheStringCopyService { private get; set; }

`Module1View` 的“复制”按钮使用此服务发送输入到 `Module1` 文本框中的文本。

        void CopyButton_Click(object sender, RoutedEventArgs e)
        {
            TheStringCopyService.Copy(TheTextToCopyTextBox.Text);
        }  

`Module2View` 通过其导入构造函数获取对“字符串复制”服务的引用(这是必要的,因为我们想确保在构造函数中有一个已初始化的服务引用)。它向服务的 `CopyStringEvent` 事件注册一个事件处理程序,以捕获字符串复制事件。在事件处理程序中,我们将复制的 `string` 分配给 `Module2View` 视图中的一个文本块。

    [Export]
    public partial class Module2View : UserControl
    {
        [ImportingConstructor]
        public Module2View([Import] IStringCopyService stringCopyService)
        {
            InitializeComponent();

            stringCopyService.CopyStringEvent += TheStringCopyService_CopyStringEvent;
        }

        void TheStringCopyService_CopyStringEvent(string copiedString)
        {
            CopiedTextTextBlock.Text = copiedString;
        }
    }

运行解决方案后,您将看到以下屏幕:

练习:创建一个类似的演示(请参阅 “Prism for Silverlight/MEF 简易教程 第一部分”,了解如何为 Silverlight Prism 应用程序和模块创建项目以及如何使用引导程序将模块加载到应用程序中)。

通过带弱事件处理程序的有服务进行模块间通信

注意:读者“stooboo”注意到,如果应用程序的生命周期内创建和移除处理服务事件的视图(例如,前一个示例中的 `IStringCopyService.CopyStringEvent`),则上述代码可能会导致内存泄漏。

有几种方法可以处理这种情况。最简单但最不安全的方法是在视图从应用程序中移除时强制移除事件处理程序。就前一个示例而言,这意味着在视图从应用程序中移除的每个代码位置调用...

TheStringCopyService.CopyStringEvent -= 
                TheStringCopyService_CopyStringEvent;

...在每个视图从应用程序中移除的代码位置。

显然,这不是一个很好的方法,因为责任落在了使用该服务的开发人员身上,他们很容易忘记在需要的地方插入清理代码。

正确的解决方案是创建一个弱事件,这样向它添加处理程序就不会阻止在视图不再被应用程序其他部分引用时对其进行垃圾回收。

在 C# 中创建弱事件有几种方法;例如,在 Weak Events in C#Solving the Problem with Events: Weak Event Handlers 中都有描述。

在这里,我使用了我自己的“穷人版”实现,仅针对 `StringCopyService`,足以展示如何实现。顺便说一句,这个实现与 Prism 的 `WeakDelegatesManager` 功能相匹配(出于某种原因,`WeakDelegatesManager` 类对 Prism 是内部的,所以我无法直接使用它)。

此示例的代码与上一个非常相似。唯一的区别是 `StringCopyServiceImpl` 类增加了功能,将 `CopyStringEvent` 转换为弱事件。此外,`Module2View` 现在有一个按钮和功能,可用于将其“`DynamicViewRegion”区域添加或移除另一个视图(`DynamicView`)。

        DynamicView _dynView = null;

        ...

        void AddOrRemoveDynamicView()
        {
            if (_dynView == null)
            {
                // adding the view

                // create the dynamic view passing the string copy service reference to it
                _dynView = new DynamicView(_stringCopyService);
                _dynView.OnDestructorCalled += new Action(_dynView_OnDestructorCalled);

                _regionManager.AddToRegion("DynamicViewRegion", _dynView);
                AddRemoveDynamicViewButton.Content = "Remove Dynamic View";
                DestructorCalledIndicatorText.Text = "";
            }
            else
            {
                // removing the view and cleaning up its references
                _regionManager.Regions["DynamicViewRegion"].Remove(_dynView);
                _dynView = null;

                // force garbage collection to 
                // try hitting the destructor right away
                GC.Collect();

                AddRemoveDynamicViewButton.Content = "Add Dynamic View";
            }
        }

`DynamicView` 类处理 `StringCopyService.CopyStringEvent`。当调用其析构函数时,它还会触发 `OnDestructorCalled` 事件。

    public partial class DynamicView : UserControl
    {
        public event Action OnDestructorCalled = null;

        public DynamicView(IStringCopyService TheStringCopyService)
        {
            InitializeComponent();

            TheStringCopyService.CopyStringEvent += 
                TheStringCopyService_CopyStringEvent;
        }

        public void TheStringCopyService_CopyStringEvent(string copiedString)
        {
            CopiedStringTextBlock.Text = copiedString;
        }

        ~DynamicView()
        {
            if (OnDestructorCalled != null)
                OnDestructorCalled();

            GC.SuppressFinalize(this);
        }
    }

`OnDestructorCalled` 事件由 `Module2View` 类处理,如果调用了析构函数,它会显示“DynamicView Destructor Called!”消息。

        void _dynView_OnDestructorCalled()
        {
            System.Windows.Deployment.Current.Dispatcher.BeginInvoke
            (
                () => DestructorCalledIndicatorText.Text = "DynamicView Destructor Called!"
            );
        }

这是 `StringCopyServiceImple` 类中的弱事件实现。

    public class StringCopyServiceImpl : IStringCopyService
    {
        #region IStringCopyService Members

        //public event Action<string> CopyStringEvent;

        List<idelegatereference> _copyStringDelegateReference =
            new List<idelegatereference>();

        public event Action<string> CopyStringEvent
        {
            add
            {
                _copyStringDelegateReference.Add
                (
                    new DelegateReference(value, false)
                );
            }

            remove
            {

            }
        }

        public void Copy(string str)
        {
            foreach (IDelegateReference del in _copyStringDelegateReference)
            {
                if (del.Target == null)
                    continue;

                if (del.Target.Target is WeakReference)
                {
                    if (!(del.Target.Target as WeakReference).IsAlive)
                        continue;
                }

                (del.Target as Action<string>)(str);
            }
        }

        #endregion
    }

Prism 的 `DelegateReference` 类用于创建引用事件处理程序的弱委托。它被添加到此类事件处理程序的列表中。调用复制操作时,我们会遍历列表中的所有 `DelegateReference` 对象,并调用相应的委托。可以看出,我们没有从列表中移除 `DelegateReference` 对象,因此仍然存在一些内存泄漏,但它们引用的视图不再被硬引用,一旦对它们的其他引用被移除,它们就会被移除。如果需要,我们可以在每次添加新的事件处理程序时清理委托列表(这将防止任何内存泄漏)。

这是左侧“复制”按钮触发复制操作后应用程序的外观。

如果按下右侧的“移除动态视图”按钮,“动态视图”将消失,您应该会在 `Module2View` 区域内看到文本“DynamicView Destructor Called”。

注意:出于某种原因,析构函数并不总是在我们置空视图的引用时被调用。我认为这与 Silverlight 的垃圾回收器的一些怪癖有关。但这与视图中的事件处理程序无关——当我们断开事件处理程序时,也会发生同样的事情。

通过区域上下文进行模块间通信

正如我们在本教程的第一部分和第二部分中学到的,一个模块可以在 `ContentControl`、`ListBox` 或其他元素上定义一个区域,而其他模块可以将其视图插入到该区域中。事实证明,我们可以定义一些数据,这些数据可以在定义区域的模块和将视图插入该区域的模块之间传递。

区域上下文示例位于“*CommunicationsViaRegionContext.sln*”解决方案下。其应用程序(主模块)使用区域上下文功能将数据传递给其 `Module1` 模块。

定义“`MyRegion1”的 `ContentControl` 位于应用程序(主模块)的 *Shell.xaml* 文件中。

          <!-- this content control defines the location for MyRegion1 region-->
          <ContentControl x:Name="TheRegionControl"
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        prism:RegionManager.RegionName="MyRegion1" />

Shell 的“复制文本”按钮的“`Click”事件处理程序定义在 *Shell.xaml.cs* 文件中。

        void TheButton_Click(object sender, RoutedEventArgs e)
        {
            // get the region context from the region defining control
            Microsoft.Practices.Prism.ObservableObject<object> regionContext =
                RegionContext.GetObservableContext(TheRegionControl);

            // set the region context's value to the string we want to copy
            regionContext.Value = TextBoxToCopyFrom.Text;
        }

从上面的代码可以看出,我们使用 `RegionContext` 类的 `static` 函数 `RegionContext.GetObservableContext` 来获取区域上下文。然后,我们将它的 `Value` 属性设置为我们要传输的数据。

在接收端 `Module1View` 中,在其构造函数中,它向区域上下文的 `PropertyChanged` 事件注册一个事件处理程序,以检测其 `Value` 属性何时发生更改。在事件处理程序中,从区域上下文中提取 `Value` 属性的值,并将其分配给模块中的文本块。

    [Export]
    public partial class Module1View : UserControl
    {
        public Module1View()
        {
            InitializeComponent();

            // get the region context from the current view 
            // (which is plugged into the region)
            Microsoft.Practices.Prism.ObservableObject<object> regionContext = 
                RegionContext.GetObservableContext(this);

            // set an event handler to run when PropertyChanged event is fired
            regionContext.PropertyChanged += regionContext_PropertyChanged;
        }

        void regionContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // if region context's Value property changed passing the 
            // text block's text property
            // the value from region context's Value property
            if (e.PropertyName == "Value")
            {
                ObservableObject<object> obj = (ObservableObject<object>)sender;

                CopiedTextTextBlock.Text = (string) obj.Value;
            }
        }
    }

这是示例窗口的外观。

上面的示例将一个 `string` 作为通信数据发送。实际上,它可以是两个模块都引用的项目中的任何对象。

有关区域上下文通信的更多信息,请参阅 Prism Regions Video Tutorial

练习:创建一个类似的演示。

通过事件聚合器进行模块间通信

事件聚合器功能允许不同模块发布和订阅(发送和接收)任何数据对象,以便在任何模块之间进行通信。与上述通信方法不同,它不需要创建服务,并且对涉及的模块没有任何限制。

示例代码位于“*CommunicationsViaEventAggregator.sln*”解决方案下。两个模块被加载到应用程序中:`Module1` 和 `Module2`。它们都引用“`Common”库项目,该项目定义了一个用于通信数据的类。

    public class MyCopyData
    {
        public string CopyString { get; set; }
    }

当用户按下“复制”按钮时,`Module1View` 对象发布数据。

        void CopyButton_Click(object sender, RoutedEventArgs e)
        {
            // get a reference to the event from 
            // the event aggregator
            CompositePresentationEvent<MyCopyData> myCopyEvent = 
                TheEventAggregator.GetEvent<CompositePresentationEvent<MyCopyData>>();

            // get the data text from TheTextToCopyTextBox TextBox control
            MyCopyData copyData = new MyCopyData
            { 
                CopyString = TheTextToCopyTextBox.Text
            };

            //publish data via event aggregator
            myCopyEvent.Publish(copyData);
        }

`Module2View` 通过事件聚合器订阅同一事件;在订阅事件处理程序中,它将接收到的 `string` 分配给其文本块的 `Text` 属性。

    [Export(typeof(Module2View))]
    public partial class Module2View : UserControl
    {
        [ImportingConstructor]
        public Module2View([Import] IEventAggregator eventAggregator)
        {
            InitializeComponent();

            eventAggregator.
                GetEvent<CompositePresentationEvent<MyCopyData>>().
                Subscribe(OnCopyDataReceived);
        }

        // should be public!
        public void OnCopyDataReceived(MyCopyData copyData)
        {
            CopiedTextTextBlock.Text = copyData.CopyString;
        }
    }

运行项目后,在左侧文本框中输入文本并按下“复制”按钮,您将看到以下内容:

如果我们遵循上面描述的发布/订阅功能,我们将无法区分传递相同类型数据的事件(该功能中没有任何内容允许我们只订阅 `MyCopyData` 类型的某些事件)。要解决此问题,Prism 引入了一个称为事件过滤的概念。Prism 提供了几个 `Subscribe(...)` 方法(我们使用了最简单的一种)。其中最复杂的一种具有以下签名:

    public virtual SubscriptionToken Subscribe
    (
        Action<TPayload> action, 
        ThreadOption threadOption, 
        bool keepSubscriberReferenceAlive, 
        Predicate<TPayload> filter
    );

其最后一个参数“`filter”是一个委托,它接受一个数据对象并返回一个布尔指示符,指定“`action”委托是否应该触发事件。可以使用不同的过滤策略:例如,订阅委托仅在数据字符串具有某种模式时触发。或者,我们可以在数据类 `MyCopyData` 中添加“`EventName”属性,并只订阅具有特定名称的事件。

`Subscribe` 函数的“`threadOption”参数指定事件将在哪个线程上传递,从一个模块到另一个模块。

  1. `BackgroundThread` 值将使用线程池中的一个线程。
  2. `PublishedThread` 将在发布事件的同一线程中处理事件(最适合调试)。这是默认选项。
  3. `UIThread` 将在应用程序的 UI 线程中执行事件处理。

第三个参数“`keepSubscriberReferenceAlive”默认为“false”。将其设置为“true”可以使订阅事件处理程序更快地调用,但需要调用 `Unsubscribe` 方法才能对事件对象进行垃圾回收。

练习:使用订阅过滤功能创建一个类似的演示。

模块间通信方法比较

本节是由于与读者“stooboo”的交流而添加的。非常感谢他注意到潜在的内存泄漏并激发了关于比较不同模块间通信方法的讨论。

事件聚合器无疑是最强大、最常用的通信方法。它允许任何模块之间进行通信(而不仅仅是同一区域层次结构中的模块),并且几乎不需要额外的功能(无需为此构建服务)。它还可以创建与视图的弱委托连接,这样就不必取消订阅即可移除视图。

然而,如上所述,Prism 事件聚合器的一个弱点是它通过类型进行事件订阅。假设我们需要传递一个 `string` 参数。当然,我们可以进行过滤,但仍然需要引入一些复杂类型,其中包含例如 `EventName` 属性,以便只获取我们需要的事件。因此,如果您创建一个第三方模块,例如一个显示日志 `string` 的窗口,我建议您将其创建一个服务,并提供相应的 API,允许用户将 `string` 作为参数传递,而不是使用事件聚合器。

区域上下文是最简单的,用于同一 `Region` 层次结构内的模块之间的通信。

致谢

我想感谢我将近 9 岁的亲爱的女儿,她在写这篇文章时,她一直在唠叨我,给我讲故事,让我测试她的乘法表知识,并威胁我(爸爸,如果你发表这个致谢,我就打你脸),证明我可以在恶劣的条件下写 Prism 文章。:)

历史

  • 2011 年 2 月 20 日 - 发布文章
  • 2011 年 2 月 22 日 - 添加了关于在服务中使用弱事件的部分,以及另一个比较不同通信方法的章节。这两个部分都是由于与读者“stooboo”的讨论而添加的。
© . All rights reserved.