Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Prism for Silverlight/MEF in Easy Samples. Part 3 - Communication between the Modules

0.00/5 (No votes)
21 Feb 2011 8  
3rd part of Prism tutorial describing communications between the modules

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). The 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 a Service with Weak Event Handlers

Note: Reader "stooboo" noticed that the code above might cause a memory leak in case when the views that handle the service's event (e.g. IStringCopyService.CopyStringEvent in the previous example) are created and removed throughout the lifetime of the application.

There are several ways to deal with such situation. The simplest but, the least safe one would be to force removal of the event handler when the view is removed from the application. In terms of the previous sample, that would mean calling...

TheStringCopyService.CopyStringEvent -= 
                TheStringCopyService_CopyStringEvent;

...at every place in the code in which the view is removed from the application.

Obviously, this is not a very good approach as the onus is placed on the developers who use the service and who can very easily forget inserting the clean up code at every place they need it.

The right solution would be to create a weak event, so that adding a handler to it would not prevent the corresponding view from being garbage collected when it is no longer referenced by other parts of the application.

There are several ways in which weak events can be created in C#; some are described in e.g. Weak Events in C# and Solving the Problem with Events: Weak Event Handlers.

Here I use my own "poor man's" implementation, specific to StringCopyService just enough to show general ideas of how it can be achieved. This implementation, by the way, matches Prism's WeakDelegatesManager functionality (for some reason, WeakDelegatesManager class is internal to Prism, so I could not use it directly).

The code for this sample is very similar to the previous one. The only difference is that StringCopyServiceImpl class has additional functionality that turns CopyStringEvent into a weak event. Also Module2View now has a button and the functionality that can be used to add or remove another view (DynamicView) to its "DynamicViewRegion" region:

        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 class is handling the StringCopyService.CopyStringEvent. It also fires OnDestructorCalled event when its destructor is called:

    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 event is handled by Module2View class which displays "DynamicView Destructor Called!" message if the destructor was called:

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

Here is the weak event implementation within StringCopyServiceImple class:

    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's DelegateReference class is employed to create a weak delegate referencing the event handler. It is added to the list of such event handlers. When copy operation is called, we iterate over all DelegateReference objects within the list calling the corresponding delegate. One can see that we are not removing the DelegateReference objects from the list, so there is still a little bit of a memory leak, but, the views that they are referring to are no longer hard referenced and will be removed once other references to them are removed. If necessary, we can do the Delegate list clean up every time we add a new event handler (this will prevent any memory leak).

This is how the application looks after copying was triggered by the "Copy" button on the left hand side:

If one presses "Remove Dynamic View" button on the right, the "Dynamic View" disappears and one should see the following text "DynamicView Destructor Called" within Module2View area:

Note: For some reason, the destructor is not called every time we null the references to the view. I think it is related to some kinks with Silverlight's garbage collector. But this has nothing to do with the event handler within the view - the same happens when I disconnect the event handler.

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

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 is 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 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.

Comparison of Inter-Module Communication Methods

This section has been added due to an exchange with reader "stooboo". A big hat tip to him for noticing the potential memory leak and stimulating the discussion about comparing different inter-module communication methods.

Event aggregator is definitely the most powerful and most widely used communication method. It enables communicating between any modules (not only those within the same region hierarchy) and without almost any extra functionality (one does not have to build a service for it). It also can create weak delegate connections to the views so that one does not have to unsubscribe in order for the views to be removed.

As was mentioned above, however, one of the weak points of Prism's event aggregator is the fact that it does event subscription by type. Suppose we need to pass a string argument. To be sure, we can do filtering, but we still need to introduce some complex type containing e.g. EventName property, in order to be able to get only events that we need. So, if you create a 3rd party module, e.g. a window displaying log strings, I would recommend you to create it as a service with the corresponding API that would allow the user to pass strings as arguments and not to use the event aggregator.

Region context is the simplest to use for communication between modules within the same Region hierarchy.

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

  • February 20, 2011 - Published the article
  • February 22, 2011 - Added a section about using weak events in Services and another section comparing different communication methods. Both were added due to a discussion with reader "stooboo".

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here