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:
- Via a Prism service: A common MEF-able service is defined in a project referenced by all the modules that use it for communications.
- Via a Prism region context: Data can be transmitted from a control containing a region to the module loaded into the region.
- 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)
{
_dynView = new DynamicView(_stringCopyService);
_dynView.OnDestructorCalled += new Action(_dynView_OnDestructorCalled);
_regionManager.AddToRegion("DynamicViewRegion", _dynView);
AddRemoveDynamicViewButton.Content = "Remove Dynamic View";
DestructorCalledIndicatorText.Text = "";
}
else
{
_regionManager.Regions["DynamicViewRegion"].Remove(_dynView);
_dynView = null;
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
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):
<!---->
<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)
{
Microsoft.Practices.Prism.ObservableObject<object> regionContext =
RegionContext.GetObservableContext(TheRegionControl);
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();
Microsoft.Practices.Prism.ObservableObject<object> regionContext =
RegionContext.GetObservableContext(this);
regionContext.PropertyChanged += regionContext_PropertyChanged;
}
void regionContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
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)
{
CompositePresentationEvent<MyCopyData> myCopyEvent =
TheEventAggregator.GetEvent<CompositePresentationEvent<MyCopyData>>();
MyCopyData copyData = new MyCopyData
{
CopyString = TheTextToCopyTextBox.Text
};
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);
}
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:
BackgroundThread
value will use a thread from the thread pool.
PublishedThread
will handle the event in the same thread in which the event was published (best for debugging). This is the default option.
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 string
s, I would recommend you to create it as a service with the corresponding API that would allow the user to pass string
s 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
".