Introduction

This is the 3rd (and last) part of "Prism for Silverlight/MEF in Easy Samples" Trilogy. It describes communications between different modules within the application.

Here are the pointers to Part 1: Prism Modules and Part 2: Prism Navigation.

This part of the tutorial assumes some knowledge of C#, MEF and Silverlight as well as the concepts of Prism modules and regions which can be learned from parts 1 and 2 of this tutorial.

As we learned in Part 1: Prism Modules, Prism modules are independently deployable software units (.dll files in WPF and .xap files in Silverlight). Main module which is used to assemble other modules is called "application".

Communication between different modules is a little bit of a challenge since most modules do not reference each other (the independence condition) and thus cannot access each other's functionality directly. The modules, however, can reference some other projects that can provide a way for them to access common communication data and communication interfaces.

There are three ways for Prism modules to communicate between each other:

  1. Via a Prism service: a common MEF-able service is defined in a project referenced by all the modules that use it for communications.
  2. Via a Prism region context: data can be transmitted from a control containing a region to the module loaded into the region.
  3. Via Prism's Event Aggregator: it is the most powerful and simple method of communication between the modules - unlike Prism service, it does not require any extra services built and unlike region context method, it can be used for communicating between any two modules, not only the modules within region hierarchy.

Inter-Module Communications Overview and Samples

This tutorial contains 3 samples - each demonstrating one of the ways in which modules can communicate between each other, described above. In all these samples a string from one module is copied into another module and displayed there.

Inter-Module Communications via a Service

The source code for this project can be found under "CommunicationsViaAService.sln" solution.

Two modules: Module1 and Module2 are loaded into the application (Main Module) by the bootstrapper. The application and the two modules are dependent on a very thin project called "Common" containing an interface for the inter-module communication service:

    public interface IStringCopyService
    {
        event Action<string> CopyStringEvent;

        void Copy(string str);
    }

The service MEF-able implementation is located within the application module:

    [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
    }

Notice that since we did not set the PartCreationPolicy attribute, the StringCopyServiceImpl will be shared by default, i.e. the StringCopyServiceImpl object will be a singleton within the solution.

This service is used to send a string from Module1 to Module2.

Module1View MEF imports a reference to this service (see Module1View.xaml.cs file):

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

Module1View's "Copy" button is using this service to send the text entered into Module1's text box:

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

Module2View gets a reference to the "String Copy" service via its importing constructor (this is necessary since we want to make sure that we have an initialized service reference within the constructor). It registers an event handler to the service's CopyStringEvent event to catch the string copy event. Within the event handler we assign the copied string to a text block within Module2View view:

    [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;
        }
    }

Once you run the solution, you'll see the following screen:

Exercise: Create a similar demo (look at "Prism for Silverlight/MEF in Easy Samples Tutorial Part1" for instructions on how to create projects for Silverlight Prism application and modules and how to load the module into the application using the bootstrapper).

Inter-Module Communications Via Region Context

As we learned in Parts 1 and 2 of this tutorial, one module can define a region over a ContentControl, ListBox or some other elements, while other modules can plug its views into that region. It turned out that we can define some data that can be passed between the module that defines a region and the module which plugs its view into that region.

The region context sample is located under "CommunicationsViaRegionContext.sln" solution. It's application (main module) uses Region Context functionality to pass data to its Module1 module.

ContentControl that defines "MyRegion1" is located in Shell.xaml file within the application (main module):

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

Shell's "Copy Text" button's "Click" event handler is defined within Shell.xaml.cs file:

        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;
        }

One can see from the code above that we get the region context by using static function RegionContext.GetObservableContext of the RegionContext class. Then we set its Value property to the data we want to transmit.

On the receiving side Module1View, within its constructor, registers an event handler with the region context's PropertyChanged event to detect when its Value property changes. Within the event handler the Value property's value is extracted from the region context and assigned to the text block within the module:

    [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 assing 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;
            }
        }
    }

Here is how the sample's window looks:

The sample above sends a string as communication data. In reality it can be any object defined in a project that both modules reference.

For more on region context communications, please look at Prism Regions Video Tutorial.

Exercise: create a similar demo.

Inter-Module Communications via the Event Aggregator

Event aggregator functionality allows different modules to publish and subscribe (to send and receive) any data objects between any modules. Unlike communication methods described above, it does not require a service creation and it does not impose any restrictions on the modules involved.

The sample code is located under "CommunicationsViaEventAggregator.sln" solution. Two modules are loaded into the application: Module1 and Module2. They both reference "Common" library project which defines a class for communication data:

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

Module1View object publishes data when the user presses "Copy" button:

        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 subscribes to the same event via the event aggregator; within the subscription event handler it assigns the received string to its text block's Text property:

    [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;
        }
    }

Here what you'll see once you run the project, enter text within the text box on the left and press the "Copy" button:

If we follow the publish/subscribe functionality described above, we won't be able to distinguish between events that pass data of the same type (there is nothing within the functionality that would allow us to subscribe to only some of the events of MyCopyData type). To fix this problem, Prism introduces a concept called event filtering. There are several Subscribe(...) methods provided by Prism (of which we used the simplest one). The most complex, of them, has the following signature:

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

The last of its arguments "filter" is a delegate that that takes a data object and returns a Boolean indicator specifying whether the "action" delegate should fire the event or not. Different filtering strategies can be employed: e.g. a subscription delegate can fire only if the data string has some pattern to it. Or, alternatively, we can add "EventName" property to our data class MyCopyData and subscribe only to events of certain name.

The "threadOption" argument to Subscribe function specifies the thread in which the event will be passed from one module to another:

  1. BackgroundThread value will use a thread from the thread pool.
  2. PublishedThread will handle the event in the same thread in which the event was published (best for debugging). This is the default option.
  3. UIThread will perform event handling in the UI thread of the application

The 3rd argument "keepSubscriberReferenceAlive" is "false" by default. Setting it to "true" can make subscription event handler invoked faster, but will require calling Unsubscribe method for the event object to be garbage collected.

Exercise: Create a similar demo using subscription filtering functionality.

Acknowledgment

I would like to thank my dear almost 9 year old daughter who was nagging me, telling me stories, asking me to test her multiplication table knowledge, threatening me (daddy if you publish this acknowledgement I am going to smack you in the face) while I was working on this article, thus proving that I can write Prism articles under adverse conditions:)

History

Feb. 20, 2011 - Published the article.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架