Click here to Skip to main content
15,860,861 members
Articles / Programming Languages / C#

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

Rate me:
Please Sign up or sign in to vote.
4.92/5 (47 votes)
21 Feb 2011CPOL11 min read 147.6K   3.3K   54   59
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:

C#
public interface IStringCopyService
{
    event Action<string> CopyStringEvent;

    void Copy(string str);
}

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

C#
[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):

C#
[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:

C#
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:

C#
[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:

Image 1

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

C#
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:

C#
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:

C#
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:

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

Here is the weak event implementation within StringCopyServiceImple class:

C#
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:

Image 2

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:

Image 3

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):

XML
<!-- 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:

C#
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:

C#
[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:

Image 4

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:

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

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

C#
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:

C#
[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:

Image 5

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:

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
QuestionMVVM implementation of this example Pin
waypoint725-Jul-11 7:30
waypoint725-Jul-11 7:30 
AnswerRe: MVVM implementation of this example Pin
Nick Polyak15-Aug-11 4:11
mvaNick Polyak15-Aug-11 4:11 
SuggestionDataServices with Silverlight WCF [modified] Pin
waypoint722-Jul-11 4:15
waypoint722-Jul-11 4:15 
GeneralRe: DataServices with Silverlight WCF [modified] Pin
Nick Polyak23-Jul-11 20:29
mvaNick Polyak23-Jul-11 20:29 
GeneralRe: DataServices with Silverlight WCF Pin
waypoint725-Jul-11 7:26
waypoint725-Jul-11 7:26 
GeneralRe: DataServices with Silverlight WCF [modified] Pin
energie21-Dec-11 3:07
energie21-Dec-11 3:07 
GeneralMy vote of 5 Pin
JF201523-Feb-11 9:07
JF201523-Feb-11 9:07 
GeneralRe: My vote of 5 [modified] Pin
Nick Polyak23-Feb-11 10:17
mvaNick Polyak23-Feb-11 10:17 
GeneralMy vote of 5 Pin
Kunal Chowdhury «IN»23-Feb-11 4:36
professionalKunal Chowdhury «IN»23-Feb-11 4:36 
GeneralRe: My vote of 5 Pin
Nick Polyak23-Feb-11 5:32
mvaNick Polyak23-Feb-11 5:32 
GeneralMy vote of 5 Pin
ACanadian22-Feb-11 5:25
ACanadian22-Feb-11 5:25 
GeneralRe: My vote of 5 Pin
Nick Polyak22-Feb-11 5:29
mvaNick Polyak22-Feb-11 5:29 
GeneralMy vote of 5 Pin
Petr Pechovic21-Feb-11 23:34
professionalPetr Pechovic21-Feb-11 23:34 
GeneralRe: My vote of 5 Pin
Nick Polyak22-Feb-11 0:08
mvaNick Polyak22-Feb-11 0:08 
Generalgood stuff again Pin
Sacha Barber21-Feb-11 19:12
Sacha Barber21-Feb-11 19:12 
GeneralRe: good stuff again Pin
Nick Polyak21-Feb-11 19:17
mvaNick Polyak21-Feb-11 19:17 
GeneralMy vote of 5 Pin
RaviRanjanKr21-Feb-11 17:41
professionalRaviRanjanKr21-Feb-11 17:41 
GeneralRe: My vote of 5 Pin
Nick Polyak21-Feb-11 17:56
mvaNick Polyak21-Feb-11 17:56 
GeneralMy vote of 5 Pin
stooboo21-Feb-11 10:59
stooboo21-Feb-11 10:59 
GeneralRe: My vote of 5 Pin
Nick Polyak21-Feb-11 11:01
mvaNick Polyak21-Feb-11 11:01 
GeneralMy vote of 4 Pin
stooboo21-Feb-11 9:56
stooboo21-Feb-11 9:56 
GeneralRe: My vote of 4 Pin
Nick Polyak21-Feb-11 10:52
mvaNick Polyak21-Feb-11 10:52 
GeneralRe: My vote of 4 Pin
stooboo21-Feb-11 11:11
stooboo21-Feb-11 11:11 
GeneralRe: My vote of 4 [modified] Pin
Nick Polyak21-Feb-11 11:25
mvaNick Polyak21-Feb-11 11:25 
GeneralRe: My vote of 4 Pin
Nick Polyak21-Feb-11 13:09
mvaNick Polyak21-Feb-11 13:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.