Prism Event DashBoard






4.77/5 (7 votes)
DashBoard to view all PRISM events in real-time

Contents
- Introduction
- Quick Start
- Background
- Design Decisions
- What's Included
- Visual Studio Setup
- Event Aggregator
- Changes in Prism
- Using the DashBoard in an Application
- Event DashBoard View
- Event DashBoard ViewModel
- New Prism Event Handlers
- Enhancement Ideas
- Utilities
- In Summary...
- History
Introduction
The Prism Event DashBoard came out of a need to better see inside of the
Prism EventAggregator
. If you are building a large Prism application or have
recently started working on a new Prism application, it can take some time to
understand the flow of events that take place. Questions like these are tough to
answer normally, but are handled nicely by the dashboard:
- Where are the events defined?
- Where are they published from?
- Where are the Event Handlers?
- How often do they get executed?
- Are they executed too often or not enough?
It is easy to miss the actual flow of events if your only recourse is to manually trace code.
The Prism Event DashBoard answers all of these questions by logging all of the Prism events in a dashboard interface and presenting summary information on how often each event handler gets executed along with detailed information on every Publish event that is raised, all the handlers that get executed for that publish, and detailed information on each handler. You are even able to examine the payload values that have been sent to each handler along with what assembly the handlers are in and their specific location within the assembly. Once you start collecting data, you may find events firing you didn't know were firing along with events that don't fire at all or too often. If your application is multi-threaded, that adds a whole other layer of complexity to the event firing sequence. This dashboard can help unravel that complexity.
The dashboard is packaged as a separate module that can be plugged into any
application. After launching your application, you can instantiate the dashboard
view. Once it is running, all events that Prism executes will be collected in
real time as the application is running. You can see the events being added to
the various list views along with tally's of the total number of subscriptions,
published events, number of event handlers being executed and the number of
UnSubscribe
operations being performed. One event being published may result in
many event handlers being executed, this dashboard will log all of them.
Quick Start
To get up and running and see what is happening right away, simply unzip the StockTrader RI.zip file into a local directory. Then just compile the solution file provided and run it to see the demo app and dashboard module in action! It contains all of the DLLs needed to run the dashboard without first having to download and install the full PRISM framework. All projects were compiled using Visual Studio 10 in .NET 4.0. I've also built it in Visual Studio 12 with .NET 4.5. But it does not use any 4.5 features yet. This is the demo application that comes with the PRISM framework and I thought this was the best one to use since everyone would get it with the framework download.
Background
The Prism framework is a WPF framework written by Microsoft and is available here. It is a comprehensive framework that can significantly increase productivity within your WPF development team. The main focus of this article will be on the event aggregator that is part of the framework. It is possible to use this event framework separately in your own application without using any of the other pieces of the Prism framework. I would recommend however, taking a look at the whole package and utilizing as much as possible. It is a wonderful piece of software. When using the event aggregator with its Publish/Subscribe pattern, you will wonder how you developed without it.
While most of this article is intermediate to advanced in content, I will not assume the reader is familiar with all of the patterns and concepts. Many Microsoft technologies are careers unto themselves and one can't know all of the technologies equally. So where deemed necessary, I will explain what may appear as basic information... just to be sure. :)
Design Decisions
In order to respond to all events that are happening inside of a Prism
application, there is only one place that the notification of the application
level events can be published. That is inside of the Prism framework itself. The
Prism framework DLL (Microsoft.Practices.Prism
) is referenced by all modules
inside of your Prism application. All events that are subscribed to and
published are handled by this core Prism DLL. So this is the gateway that needs
to be modified in order to log these application level events.
I created four new events within Prism itself to track subscription, publishing, execution and unsubscribing of all application level events:
New Events
New Event | Description |
PrismSubscribeNotificationEvent |
Is published by Prism when
your application code performs a subscribe:
eventAggregator.GetEvent<MarketPricesUpdatedEvent>().
Subscribe(MarketPricesUpdated, ThreadOption.UIThread));
|
PrismPublishNotificationEvent |
Is published by Prism when your application code performs a Publish:
eventAggregator.GetEvent<TickerSymbolSelectedEvent>().
Publish((CurrentPositionSummaryItem.TickerSymbol));
|
PrismSubscriberExecutionNotificationEvent |
Is published by Prism when it calls all of the subscribers to the event being Published. |
PrismUnSubscribeNotificationEvent |
Is published by Prism when an event is unSubscribed in your application. |
These new events are created in a new "PrismEvents" directory within the prism directory structure. By using these new events and the modifications in the core prism project, NO changes are necessary inside of your existing code base. All that is done is to drop in the Dashboard module into your application and wire it up to a menu or inject it into a region. When the dashboard is running, all it is doing is listening for these 4 new events that are being published by Prism itself. It keeps running stats on them and populates the summary and detailed list views with the event data as your events are firing.
The dashboard view itself for the purposes of this article is kept pretty generic. To make the dashboard view look like a dash board, it would be a good idea to add in controls like gauges, etc. to give it a more visual appeal, but to keep it easy to plug in and use, these kinds of things were left out for this article.
In order to utilize these new events, there were
updates to a few prism classes but not very much was needed once it was fine
tuned. The main class for events, "CompositePresentationEvent
" was not
subclassed to minimize changes within Prism. The more important goal was to
minimize the changes to your applications. If this core class was subclassed, all
of the references to it would have to be changed in your application when
subscribing to all of your events. Since some Prism applications could contain
100s of events, I did not want to impose this burden on the developer. It was
far easier to make the changes within Prism.
What's Included
What's included in the zip files for this article are the new Prism framework
DLLs. These are Microsoft.Practices.Prism
,
Microsoft.Practices.Prism.Interactivity
and
Microsoft.Practices.Prism.MefExtensions
. The only actual changes are in the
Microsoft.Practices.Prism
assembly, but all of the original DLLs that are in the
Prism download from Microsoft are all strongly signed. If only one assembly is
recompiled, it will not work with the other strongly signed assemblies.
Microsoft does not provide the security certificate so they cannot be strongly
signed again unless you get your own certificate. With the Prism DLLs provided,
you'll be able to run the StockTrader
demo right away. But of course what is
also included are the new event classes and the changes to the existing Prism
classes. I am not including a whole new copy of the Prism download. You need to
download the Prism zip file from here, unzip it and then copy in the changes to
the few Prism classes (discussed below). There are not many. You will get the
modified Prism classes as well as the four (4) new Prism Events.
This is the DashBoard module. As you can see, it's a pretty small module. It is another project in the demo application. But it can also be kept in its own solution of course if you make it available to multiple applications. You just need to reference the one SoftwareLifeCycle.Modules.DashBoard.dll file to implement it.
There is minimal changes to the Stock Trader demo app. I just
added the new Dashboard module and created a new region in the main shell.xaml
file to house the new dashboard view. This is explained in detail below. It appears below the main region and
is displayed when the app launches. This way, you can see all of the events from the
beginning. In a real world LOB application, you may decide to put the dashboard on
another tab or in a dockable window that can be docked to the side of the main
window. I cleaned up the Stocktrader solution to make the download smaller. I
deleted the test projects for each of the modules. I also excluded the
ChartControls
project from the solution and instead just reference the
StockTraderRI.ChartControls.dll library where it is needed. I created a special
"libs" folder under the StockTrader
project directory to hold the ChartControls
DLL and the modified PRISM libraries along with the Enterprise Libraries
referenced in the main project. This would also be a good place to put any
additional 3rd party libraries like the XCeed WPF control libraries that are
mentioned at the end of the article. In short, this demo is self contained with
all required DLLs and has been zipped after being run through the CleanProject
utility mentioned at the end of the article. The "CleanProject
" utility cleans
out all of the bind\debug files and other files created during the build
process.
Visual Studio Setup
The changes to the Prism framework outlined below all happen inside the \PrismFrameWork\PrismLibrary\Desktop\Prism\Events and the new directory at \PrismFrameWork\PrismLibrary\Desktop\Prism\PrismEvents. The latter directory needs to be added and the new PRISM events dropped in there. Then add them to the Prism-Desktop project file so they can be referenced by the changes to other existing Prism classes.
If any additional custom changes are made to the Prism library above what is
shown here, you will need to recompile the PrismLibrary.Desktop
project. The
other Microsoft.Practices.Prism.Interactivity
and
Microsoft.Practices.Prism.MefExtensions
would not have to be recompiled if you
are using the ones supplied here.
In my own copy of the Prism Framework, I modified the three Prism projects that are needed for this Dashboard. I added these lines:
copy $(TargetPath)
"C:\Users\Harold\Documents\VisualStudio\PrismFrameWork\Builds\$(TargetName).DLL"
copy $(TargetDir)$(TargetName).pdb
"C:\Users\Harold\Documents\VisualStudio\PrismFrameWork\Builds\$(TargetName).pdb"
in the Post build event. This copies out each of the three DLLs to a central directory to hold the necessary framework libraries for the demo application. When these are used for your own projects, this approach is recommended. It makes it easier for your applications to reference the necessary DLLs in one location instead of them being spread out in various bin\debug directories. Just change the above path to reflect your central "build" directory location.
The Event Aggregator
The design philosophy behind an event aggregator is to loosely couple different modules of an application together. This makes it possible for one part of an application to simply raise an event and have the event aggregator lookup that event and publish it. There could be 1 or a 100 different event handling methods then called by the event aggregator to handle that event in different ways.
The Event Aggregator portion of the Prism framework is defined in the
PrismLibrary\Desktop\Prism\Events\EventAggregator
class. The aggregator is
simply defined as follows:
private readonly Dictionary<Type, EventBase><Type, EventBase> events = new Dictionary<Type, EventBase>();
This simply creates a dictionary that is keyed off of an Event Type and stores
the definition of the event (EventBase
) as the value for the key.
To start off the process of populating the event aggregator, a subscription to an event is made that associates an event with an event handler (method):
This code snippet is from StockTraderRI.Modules.Position.PositionSummary.ObservablePosition
class. The first parameter of the "Subscribe
" method, is the name of the
method that will handle this event. Note that this is not a literal string, it
is a reference to a method that has to be resolved in the editor! This is passed
as a generic "Action
" type. Since this reference is resolved by the compiler, it
knows where the target event handler resides. The aggregator is really just a
dictionary of these method references stored as delegates.
The following screenshot illustrates the kind of information the action delegate
tracks. It knows everything it needs to find and execute a method when the
delegates "invoke
" method is called.
Using the code snippet below, calling the GetEvent
method of the aggregator first checks to see if the
event (TickerSymbolSelectedEvent
) is already in the event dictionary. If it is not, it is added and
returned. If it already is in the dictionary, the event is returned from the
dictionary using the Type of the event as the key.
When an event is published like this:
eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish(CurrentPositionSummaryItem.TickerSymbol);
This object, which is of type "CompositePresentationEvent
"
as shown below, then executes its "Publish
" method and passes the payload,
CurrentPositionSummaryItem.TickerSymbol
, along as the payload. The
"TickerSymbolSelectedEvent
" is defined as follows:
using Microsoft.Practices.Prism.Events;
namespace StockTraderRI.Infrastructure
{
public class TickerSymbolSelectedEvent : CompositePresentationEvent<string>
{
}
}
This event is a very simple one. Its main purpose is to simply pass a string
along to the event handler that has subscribed to this event being published. In
this case, it is just passing a string
, but that string
parameter could just as
easily be a more complex object with any number of properties defined.
If there are more than one subscriber to an event, there is a list of
subscribers maintained for this event in the "EventBase
" class.
private readonly List<IEventSubscription> _subscriptions = new List<IEventSubscription>();
Then as subscriptions are made to an event, the "internalSubscribe
" method in
EventBase
simply adds another entry for this subscription to the list. This is
one of the beauties of the publish/subscribe approach. Multiple modules can
subscribe to an event, and the event aggregator maintains a list of all of the
locations by keeping the delegate reference in the above List<>
. Then during
publish, it simply iterates over the list of subscriptions and invokes the
delegate. Since the delegate has all the information that .NET needs to locate
the method being subscribed to, it can find it and execute it no matter where it
is.

Changes in Prism
There are very few changes made to the Prism source code for this new functionality. The changes are very easy to merge into the Prism 4.0 or 4.1 source code. There is very little difference between these 2 versions and by using WinMerge or comparable merge utility, it is a very easy process to create the updated code. The article download includes the new event classes and the modified Prism classes. It is recommended to unzip the supplied source code changes into a directory, download the original Prism source code, and do a file compare between the two. You will be comparing the \PrismLibrary\Desktop\Prism directory in the original source code with the updated source code provided in this article.
The following is a list of the files that have been added or modified in the Prism Framework to publish the new events. WinMerge (great free program) was used to do a file compare between the supplied modified files on the left and the original Prism files on the right.
Here is the directory structure for the new and updated files as supplied in this articles ZIP file. Under "NewUpdatedPrismFiles", there is a directory "PrismEvents" which are the new events shown above and there is an "Events" directory with the modified Prism classes. After you download the full PRISM framework, add in the new PrismEvents files and update the existing original Prism classes with the modified ones supplied. Very easy process!
Like all regular application level events, these new events are based on the
Prism CompositePresentationEvent
definition. As we'll see later, these events
are subscribed to by the Event Dashboard viewmodel so they can update the
statistics and event detail listings.
using System;
using Microsoft.Practices.Prism.Events;
namespace Microsoft.Practices.Prism.PrismEvents
{
/// <summary>
/// Event used to notify Prism Event Visualizer when a subscription is performed.
/// </summary>
public class PrismSubscribeNotificationEvent : CompositePresentationEvent<PrismSubscribeNotificationEventArgs>
{
}
/// <summary>
/// Arguments for Prism Subscribe Event
/// </summary>
public class PrismSubscribeNotificationEventArgs : EventArgs
{
public object EventObj { get; set; }
public object AppEventPayLoad { get; set; }
public int SubscriptionToken { get; set; }
public Delegate TargetReference { get; set; }
/// <summary>
///
/// </summary>
/// <param name="eventObj">reference for application event being published</param>
/// <param name="subscriptionToken">unique GUID token for this subscription</param>
/// <param name="targetReference">The Target of the subscription.. what is going to be executed.</param>
/// <param name="appEventPayLoad">Payload of the application level event.
/// used to display payload values in dashboard.</param>
public PrismSubscribeNotificationEventArgs(object eventObj,
int subscriptionToken, Delegate targetReference = null, object appEventPayLoad = null)
{
EventObj = eventObj;
AppEventPayLoad = appEventPayLoad;
SubscriptionToken = subscriptionToken;
TargetReference = targetReference;
}
}
}
using System;
using Microsoft.Practices.Prism.Events;
namespace Microsoft.Practices.Prism.PrismEvents
{
/// <summary>
/// Event that will be used to Publish to PrismEventVisualizer when an application Publish occurs.
/// </summary>
public class PrismPublishNotificationEvent : CompositePresentationEvent<PrismPublishNotificationEventArgs>
{
}
/// <summary>
/// Custom Prism Event for Publish Notification
/// </summary>
public class PrismPublishNotificationEventArgs : EventArgs
{
public object EventObj { get; set; }
public object AppEventPayLoad { get; set; }
public string CallingClass { get; set; }
public string CallingMethod { get; set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="eventObj">reference for application event being published</param>
/// <param name="callingClass">name of class where .Publish() was executed</param>
/// <param name="callingMethod">name of Method where Publish() was executed</param>
/// <param name="appEventPayLoad">Payload object from Application level event</param>
public PrismPublishNotificationEventArgs(object eventObj,
string callingClass = "", string callingMethod = "", object appEventPayLoad = null)
{
EventObj = eventObj;
AppEventPayLoad = appEventPayLoad;
CallingClass = callingClass;
CallingMethod = callingMethod;
}
}
}
using System;
using Microsoft.Practices.Prism.Events;
namespace Microsoft.Practices.Prism.PrismEvents
{
/// <summary>
/// Event that will be used to Publish to PrismEventVisualizer when an application Publish occurs.
/// </summary>
public class PrismSubscriberExecutionNotificationEvent :
CompositePresentationEvent<PrismSubscriberExecutionNotificationEventArgs>
{
}
/// <summary>
/// Arguments for when Prism Executes an event handler for an event Publish
/// </summary>
public class PrismSubscriberExecutionNotificationEventArgs : EventArgs
{
public object EventObj { get; set; }
public object AppEventPayLoad { get; set; }
public int SubscriptionToken { get; set; } // HashCode for event token
// that published the event that caused execution of this method.
public Delegate TargetReference { get; set; }
public ThreadOption ThreadingType { get; set; }
/// <summary>
///
/// </summary>
/// <param name="eventObj">reference for application event being published</param>
/// <param name="subscriptionToken">unique GUID token for this subscription</param>
/// <param name="targetReference">The Target of the subscription.. what is going to be executed.</param>
/// <param name="appEventPayLoad">Payload of the application level event.
/// used to display payload values in dashboard.</param>
/// <param name="threadingType">Threading type used to run eventhandler.
/// Displayed in dash board grid.</param>
public PrismSubscriberExecutionNotificationEventArgs(object eventObj,
int subscriptionToken, Delegate targetReference = null, object appEventPayLoad = null,
ThreadOption threadingType = ThreadOption.PublisherThread)
{
EventObj = eventObj;
AppEventPayLoad = appEventPayLoad;
SubscriptionToken = subscriptionToken;
TargetReference = targetReference;
ThreadingType = threadingType;
}
}
}
using System;
using Microsoft.Practices.Prism.Events;
namespace Microsoft.Practices.Prism.PrismEvents
{
/// <summary>
/// Event used to notify Prism Event Visualizer when an event is unsubscribed.
/// </summary>
public class PrismUnSubscribeNotificationEvent :
CompositePresentationEvent<PrismUnSubscribeNotificationEventArgs>
{
}
/// <summary>
/// Arguments for a prism UnSubscribe
/// </summary>
public class PrismUnSubscribeNotificationEventArgs : EventArgs
{
public object EventObj { get; set; }
public string CallingClass { get; set; }
public string CallingMethod { get; set; }
/// <summary>
///
/// </summary>
/// <param name="eventObj">reference for application event being published</param>
/// <param name="callingClass">Class that issues contains method that unsubscribes event</param>
/// <param name="callingMethod">Method that unSubscribes event.</param>
public PrismUnSubscribeNotificationEventArgs
(object eventObj, string callingClass = "", string callingMethod = "")
{
EventObj = eventObj;
CallingClass = callingClass;
CallingMethod = callingMethod;
}
}
}
Moving forward, the file differences in the listings will show the new and modified code in bold red.
CompositePresentationEvent
is the class that all application level events
inherit from.
The changes from the Prism source are the following:
- Three new
using
directives have been added. - An "
eventAggregator
"private
field has been added to hold reference to theeventAggregator
collection. - A new constructor was added to obtain the reference to the
eventAggregator
for subsequent publishing of the new Prism Events described above. - In the "
Subscribe
" method, a new block of code was added that publishes the new "PrismSubscribeNotificationEvent
" that notifies the Event Dashboard that an application level Subscription has taken place. - In the "
Publish
" method, a new block of code was added that publishes the new "PrismPublishNotificationEvent
" that notifies the Event Dashboard that an application level Publish has taken place. As part of the event payload, it also determines the calling class and method of the application level publish. This is so you know exactly where it was called from. Also passed along with this payload is the application level payload that is passed into thePublish
method from the application. This is used on the DashBoard to display the values of the application level payload values. This is very helpful in debugging events. Also a reference to the application level itself is passed along.' - Finally in the "
UnSubscribe
" method, a new block of code has been added to publish to the dashboard when an application level un-subscribe has been executed on an event. The calling class and method are also passed along in the payload along with the application level event reference.
//===================================================================================
// Microsoft patterns & practices
// Composite Application Guidance for Windows Presentation Foundation and Silverlight
//===================================================================================
// Copyright (c) Microsoft Corporation. All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious. No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===================================================================================
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows.Threading;
using Microsoft.Practices.Prism.PrismEvents;
using Microsoft.Practices.ServiceLocation;
namespace Microsoft.Practices.Prism.Events
{
/// <summary>
/// Defines a class that manages publication and subscription to events.
/// </summary>
/// <typeparam name="TPayload">The type of message
/// that will be passed to the subscribers.</typeparam>
public class CompositePresentationEvent<TPayload> : EventBase
{
private IDispatcherFacade uiDispatcher;
private IEventAggregator eventAggregator;
/// <summary>
/// Gets the <see cref="Dispatcher"/> that is bound to the UI elements.
/// </summary>
/// <value>The <see cref="Dispatcher"/> to use when
/// subscribing using <see cref="ThreadOption.UIThread"/>.</value>
private IDispatcherFacade UIDispatcher
{
get
{
if (uiDispatcher == null)
{
this.uiDispatcher = new DefaultDispatcher();
}
return uiDispatcher;
}
}
/// <summary>
/// Constructor used to get reference to eventaggregator.
/// </summary>
public CompositePresentationEvent()
{
// need to use servicelocator in Try/Catch instead of checking if ServiceLocator.Current is !null
// because of this: http://commonservicelocator.codeplex.com/workitem/9676 known problem in PRISM.
try
{
eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
}
catch (Exception ex)
{
Trace.TraceError("Error occurred getting event aggregator: {0}", ex);
}
}
/// <summary>
/// Subscribes a delegate to an event that will be published on the
/// <see cref="ThreadOption.PublisherThread"/>.
/// <see cref="CompositePresentationEvent{TPayload}"/> will maintain a
/// <seealso cref="WeakReference"/> to the target of the supplied <paramref name="action"/> delegate.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is published.</param>
/// <returns>A <see cref="SubscriptionToken"/> that uniquely identifies the added subscription.</returns>
/// <remarks>
/// The CompositePresentationEvent collection is thread-safe.
/// </remarks>
public SubscriptionToken Subscribe(Action<TPayload> action)
{
return Subscribe(action, ThreadOption.PublisherThread);
}
/// <summary>
/// Subscribes a delegate to an event.
/// CompositePresentationEvent will maintain a <seealso cref="WeakReference"/>
/// to the Target of the supplied <paramref name="action"/> delegate.
/// </summary>
/// <param name="action">
/// The delegate that gets executed when the event is raised.</param>
/// <param name="threadOption">
/// Specifies on which thread to receive the delegate callback.</param>
/// <returns>A <see cref="SubscriptionToken"/>
/// that uniquely identifies the added subscription.</returns>
/// <remarks>
/// The CompositePresentationEvent collection is thread-safe.
/// </remarks>
public SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption)
{
return Subscribe(action, threadOption, false);
}
/// <summary>
/// Subscribes a delegate to an event that will be published on the
/// <see cref="ThreadOption.PublisherThread"/>.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is published.</param>
/// <param name="keepSubscriberReferenceAlive">When <see langword="true"/>,
/// the <seealso cref="CompositePresentationEvent{TPayload}"/> keeps a reference
/// to the subscriber so it does not get garbage collected.</param>
/// <returns>A <see cref="SubscriptionToken"/>
/// that uniquely identifies the added subscription.</returns>
/// <remarks>
/// If <paramref name="keepSubscriberReferenceAlive"/>
/// is set to <see langword="false" />,
/// <see cref="CompositePresentationEvent{TPayload}"/>
/// will maintain a <seealso cref="WeakReference"/>
/// to the Target of the supplied <paramref name="action"/> delegate.
/// If not using a WeakReference (<paramref name="keepSubscriberReferenceAlive"/> is
/// <see langword="true" />), the user must explicitly call Unsubscribe for the event when
/// disposing the subscriber in order to avoid memory leaks or unexpected behavior.
/// <para/>
/// The CompositePresentationEvent collection is thread-safe.
/// </remarks>
public SubscriptionToken Subscribe(Action<TPayload> action, bool keepSubscriberReferenceAlive)
{
return Subscribe(action, ThreadOption.PublisherThread, keepSubscriberReferenceAlive);
}
/// <summary>
/// Subscribes a delegate to an event.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is published.</param>
/// <param name="threadOption">Specifies on which thread to receive the delegate callback.</param>
/// <param name="keepSubscriberReferenceAlive">When <see langword="true"/>,
/// the <seealso cref="CompositePresentationEvent{TPayload}"/> keeps a reference
/// to the subscriber so it does not get garbage collected.</param>
/// <returns>A <see cref="SubscriptionToken"/> that uniquely identifies the added subscription.</returns>
/// <remarks>
/// If <paramref name="keepSubscriberReferenceAlive"/> is set to <see langword="false" />,
/// <see cref="CompositePresentationEvent{TPayload}"/> will maintain a
/// <seealso cref="WeakReference"/> to the Target of the supplied <paramref name="action"/> delegate.
/// If not using a WeakReference (<paramref name="keepSubscriberReferenceAlive"/> is
/// <see langword="true" />), the user must explicitly call Unsubscribe for the event
/// when disposing the subscriber in order to avoid memory leaks or unexpected behavior.
/// <para/>
/// The CompositePresentationEvent collection is thread-safe.
/// </remarks>
public SubscriptionToken Subscribe(Action<TPayload> action,
ThreadOption threadOption, bool keepSubscriberReferenceAlive)
{
return Subscribe(action, threadOption, keepSubscriberReferenceAlive, null);
}
/// <summary>
/// Subscribes a delegate to an event.
/// </summary>
/// <param name="action">The delegate that gets executed when the event is published.</param>
/// <param name="threadOption">Specifies on which thread to receive the delegate callback.</param>
/// <param name="keepSubscriberReferenceAlive">When <see langword="true"/>,
/// the <seealso cref="CompositePresentationEvent{TPayload}"/> keeps a reference
/// to the subscriber so it does not get garbage collected.</param>
/// <param name="filter">Filter to evaluate
/// if the subscriber should receive the event.</param>
/// <returns>A <see cref="SubscriptionToken"/> that uniquely identifies the added subscription.</returns>
/// <remarks>
/// If <paramref name="keepSubscriberReferenceAlive"/> is set to <see langword="false" />,
/// <see cref="CompositePresentationEvent{TPayload}"/> will maintain a
/// <seealso cref="WeakReference"/> to the Target
/// of the supplied <paramref name="action"/> delegate.
/// If not using a WeakReference (<paramref name="keepSubscriberReferenceAlive"/>
/// is <see langword="true" />), the user must explicitly call Unsubscribe for the event
/// when disposing the subscriber in order to avoid memory leaks or unexpected behavior.
///
/// The CompositePresentationEvent collection is thread-safe.
/// </remarks>
public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption,
bool keepSubscriberReferenceAlive, Predicate<TPayload> filter)
{
IDelegateReference actionReference = new DelegateReference(action, keepSubscriberReferenceAlive);
IDelegateReference filterReference;
if (filter != null)
{
filterReference = new DelegateReference(filter, keepSubscriberReferenceAlive);
}
else
{
filterReference = new DelegateReference(new Predicate<TPayload>(delegate { return true; }), true);
}
EventSubscription<TPayload> subscription;
<a id="#ThreadChoice">switch (threadOption)
{
case ThreadOption.PublisherThread:
subscription = new EventSubscription<TPayload>(actionReference, filterReference);
break;
case ThreadOption.BackgroundThread:
subscription = new BackgroundEventSubscription<TPayload>(actionReference, filterReference);
break;
case ThreadOption.UIThread:
subscription = new DispatcherEventSubscription<TPayload>(actionReference, filterReference, UIDispatcher);
break;
default:
subscription = new EventSubscription<TPayload>(actionReference, filterReference);
break;
}
SubscriptionToken token = base.InternalSubscribe(subscription, threadOption);
// don't notify about subscribing to the Prism events!
if (eventAggregator != null && !(this is PrismUnSubscribeNotificationEvent)
&& !(this is PrismSubscribeNotificationEvent) && !(this is PrismPublishNotificationEvent)
&& !(this is PrismSubscriberExecutionNotificationEvent))
{
var internalSubscribeNotificationEvent = eventAggregator.GetEvent<PrismSubscribeNotificationEvent>();
// call internalpublish so Prism Event Visualizer can handle it!
if (internalSubscribeNotificationEvent != null)
{
internalSubscribeNotificationEvent.InternalPublish(new PrismSubscribeNotificationEventArgs
(subscriptionToken: token.GetHashCode(), targetReference: subscription.Action, eventObj: this));
}
}
return token;
}
/// <summary>
/// Publishes the <see cref="CompositePresentationEvent{TPayload}"/>.
/// </summary>
/// <param name="payload">Message to pass to the subscribers.</param>
public virtual void Publish(TPayload payload)
{
// publish original event and PrismSubscriberExecutionNotificationEvent for each subscriber...
// filter out publishing notification of these internal logging events!
try
{
if (eventAggregator != null && !(this is PrismUnSubscribeNotificationEvent)
&& !(this is PrismSubscribeNotificationEvent) && !(this is PrismPublishNotificationEvent)
&& !(this is PrismSubscriberExecutionNotificationEvent))
{
// get calling method name. one level up from this...
StackTrace stackTrace = new StackTrace();
string callingClass = string.Empty;
string callingMethodName = string.Empty;
if (stackTrace != null && stackTrace.GetFrames().Count() >
2 && stackTrace.GetFrame(1).GetMethod().DeclaringType != null)
{
callingClass = stackTrace.GetFrame(1).GetMethod().DeclaringType.FullName;
callingMethodName = stackTrace.GetFrame(1).GetMethod().Name;
}
// Get Internal publish Event...
var internalPublishNotificationEvent = eventAggregator.GetEvent<PrismPublishNotificationEvent>();
// call internalpublish so Prism Event Visualizer can handle it!
// this publishes the notification that an application level event was published....
if (internalPublishNotificationEvent != null)
{
internalPublishNotificationEvent.InternalPublish(new PrismPublishNotificationEventArgs
(eventObj: this, callingClass: callingClass, callingMethod: callingMethodName, appEventPayLoad: payload));
}
}
}
catch (Exception ex)
{
Trace.TraceError("Error occurred during publish notification: {0}", ex);
}
base.InternalPublish(payload);
}
/// <summary>
/// Removes the first subscriber matching <seealso cref="Action{TPayload}"/> from the subscribers' list.
/// </summary>
/// <param name="subscriber">The <see cref="Action{TPayload}"/>
/// used when subscribing to the event.</param>
public virtual void Unsubscribe(Action<TPayload> subscriber)
{
lock (Subscriptions)
{
IEventSubscription eventSubscription = Subscriptions.Cast<EventSubscription<TPayload>>().
FirstOrDefault(evt => evt.Action == subscriber);
if (eventSubscription != null)
{
Subscriptions.Remove(eventSubscription);
if (eventAggregator != null && !(this is PrismUnSubscribeNotificationEvent) &&
!(this is PrismSubscribeNotificationEvent) && !(this is PrismPublishNotificationEvent) &&
!(this is PrismSubscriberExecutionNotificationEvent))
{
// get calling method name. one level up from this...
StackTrace stackTrace = new StackTrace();
string callingClass = stackTrace.GetFrame(1).GetMethod().DeclaringType.FullName;
string callingMethodName = stackTrace.GetFrame(1).GetMethod().Name;
// Get Internal publish Event...
var internalUnSubscribeNotificationEvent = eventAggregator.GetEvent<PrismUnSubscribeNotificationEvent>();
// call internalpublish so Prism Event Visualizer can handle it!
// this publishes the notification that an application level un-subscribe was published....
if (internalUnSubscribeNotificationEvent != null)
{
internalUnSubscribeNotificationEvent.InternalPublish
(new PrismUnSubscribeNotificationEventArgs(eventObj: this,
callingClass: callingClass, callingMethod: callingMethodName));
}
}
}
}
}
/// <summary>
/// Returns <see langword="true"/>
/// if there is a subscriber matching <seealso cref="Action{TPayload}"/>.
/// </summary>
/// <param name="subscriber">The <see cref="Action{TPayload}"/>
/// used when subscribing to the event.</param>
/// <returns><see langword="true"/> if there is an
/// <seealso cref="Action{TPayload}"/> that matches;
/// otherwise <see langword="false"/>.</returns>
public virtual bool Contains(Action<TPayload> subscriber)
{
IEventSubscription eventSubscription;
lock (Subscriptions)
{
eventSubscription = Subscriptions.Cast<EventSubscription<TPayload>>().
FirstOrDefault(evt => evt.Action == subscriber);
}
return eventSubscription != null;
}
}
}
IEventSubscription
is the interface for EventSubscription
,
DispatcherEventSubscription
and BackgroundEventSubscription
. The only change in
this interface is the addition of the EventRef
property. This is used to store
the reference to the application level event so it can be sent as part of the
payload to the Event DashBoard
.
using System;
namespace Microsoft.Practices.Prism.Events
{
///<summary>
/// Defines a contract for an event subscription to be used by <see cref="EventBase"/>.
///</summary>
public interface IEventSubscription
{
/// <summary>
/// Gets or sets a <see cref="Events.SubscriptionToken"/>
/// that identifies this <see cref="IEventSubscription"/>.
/// </summary>
/// <value>A token that identifies this <see cref="IEventSubscription"/>.</value>
SubscriptionToken SubscriptionToken { get; set; }
/// <summary>
/// store EventObj with subscription so it can be sent with PrismSubscriberExecutionNotificationEvent.
/// </summary>
EventBase EventRef { get; set; }
/// <summary>
/// Gets the execution strategy to publish this event.
/// </summary>
/// <returns>An <see cref="Action{T}"/> with the execution strategy, or
/// <see langword="null" /> if the
/// <see cref="IEventSubscription"/> is no longer valid.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage
("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
Action<object[]> GetExecutionStrategy();
}
}
In EventSubscription
, there are only a few updates. The addition of 3 new
namespaces, the setting of the "EventRef
" from the interface above, and calling
the new method "PublishSubscriptionExecutionNotificationEvent
" from the
"InvokeAction
" method.
//===================================================================================
// Microsoft patterns & practices
// Composite Application Guidance for Windows Presentation Foundation and Silverlight
//===================================================================================
// Copyright (c) Microsoft Corporation. All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious. No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===================================================================================
using System;
using System.Globalization;
using Microsoft.Practices.Prism.PrismEvents;
using Microsoft.Practices.Prism.Properties;
using Microsoft.Practices.ServiceLocation;
using System.Diagnostics;
namespace Microsoft.Practices.Prism.Events
{
/// <summary>
/// Provides a way to retrieve a <see cref="Delegate"/> to execute an action depending
/// on the value of a second filter predicate that returns true if the action should execute.
/// </summary>
/// <typeparam name="TPayload">The type to use for the generic
/// <see cref="System.Action{TPayload}"/> and <see cref="Predicate{TPayload}"/> types.
/// </typeparam>
public class EventSubscription<TPayload> : IEventSubscription
{
private readonly IDelegateReference _actionReference;
private readonly IDelegateReference _filterReference;
// keep reference to Event to pass when published.
public EventBase EventRef { get; set; }
///<summary>
/// Creates a new instance of <see cref="EventSubscription{TPayload}"/>.
///</summary>
///<param name="actionReference">A reference to a delegate of type
///<see cref="System.Action{TPayload}"/>.</param>
///<param name="filterReference">A reference to a delegate of type
///<see cref="Predicate{TPayload}"/>.</param>
///<exception cref="ArgumentNullException">When <paramref name="actionReference"/> or
///<see paramref="filterReference"/> are <see langword="null" />.</exception>
///<exception cref="ArgumentException">When the target of
///<paramref name="actionReference"/> is not of type <see cref="System.Action{TPayload}"/>,
///or the target of <paramref name="filterReference"/>
///is not of type <see cref="Predicate{TPayload}"/>.
///</exception>
public EventSubscription(IDelegateReference actionReference, IDelegateReference filterReference)
{
if (actionReference == null)
throw new ArgumentNullException("actionReference");
if (!(actionReference.Target is Action<TPayload>))
throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
Resources.InvalidDelegateRerefenceTypeException, typeof
(Action<TPayload>).FullName), "actionReference");
if (filterReference == null)
throw new ArgumentNullException("filterReference");
if (!(filterReference.Target is Predicate<TPayload>))
throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
Resources.InvalidDelegateRerefenceTypeException,
typeof(Predicate<TPayload>).FullName), "filterReference");
_actionReference = actionReference;
_filterReference = filterReference;
}
/// <summary>
/// Gets the target <see cref="System.Action{T}"/>
/// that is referenced by the <see cref="IDelegateReference"/>.
/// </summary>
/// <value>An <see cref="System.Action{T}"/> or <see langword="null" />
/// if the referenced target is not alive.</value>
public Action<TPayload> Action
{
get { return (Action<TPayload>)_actionReference.Target; }
}
/// <summary>
/// Gets the target <see cref="Predicate{T}"/>
/// that is referenced by the <see cref="IDelegateReference"/>.
/// </summary>
/// <value>An <see cref="Predicate{T}"/> or
/// <see langword="null" /> if the referenced target is not alive.</value>
public Predicate<TPayload> Filter
{
get { return (Predicate<TPayload>)_filterReference.Target; }
}
/// <summary>
/// Gets or sets a <see cref="Events.SubscriptionToken"/>
/// that identifies this <see cref="IEventSubscription"/>.
/// </summary>
/// <value>A token that identifies this <see cref="IEventSubscription"/>.</value>
public SubscriptionToken SubscriptionToken { get; set; }
/// <summary>
/// Gets the execution strategy to publish this event.
/// </summary>
/// <returns>An <see cref="System.Action{T}"/>
/// with the execution strategy, or <see langword="null" />
/// if the <see cref="IEventSubscription"/> is no longer valid.</returns>
/// <remarks>
/// If <see cref="Action"/> or <see cref="Filter"/> are no longer valid because they were
/// garbage collected, this method will return <see langword="null" />.
/// Otherwise it will return a delegate that evaluates the <see cref="Filter"/> and if it
/// returns <see langword="true" /> will then call <see cref="InvokeAction"/>. The returned
/// delegate holds hard references to the <see cref="Action"/> and <see cref="Filter"/> target
/// <see cref="Delegate">delegates</see>. As long as the returned delegate is not garbage collected,
/// the <see cref="Action"/> and <see cref="Filter"/> references delegates won't get collected either.
/// </remarks>
public virtual Action<object[]> GetExecutionStrategy()
{
Action<TPayload> action = this.Action;
Predicate<TPayload> filter = this.Filter;
if (action != null && filter != null)
{
return arguments =>
{
TPayload argument = default(TPayload);
if (arguments != null && arguments.Length > 0 && arguments[0] != null)
{
argument = (TPayload)arguments[0];
}
if (filter(argument))
{
InvokeAction(action, argument);
}
};
}
return null;
}
/// <summary>
/// Invokes the specified <see cref="System.Action{TPayload}"/> synchronously when not overridden.
/// </summary>
/// <param name="action">The action to execute.</param>
/// <param name="argument">The payload to pass
/// <paramref name="action"/> while invoking it.</param>
/// <exception cref="ArgumentNullException">An <see cref="ArgumentNullException"/>
/// is thrown if <paramref name="action"/> is null.</exception>
public virtual void InvokeAction(Action<TPayload> action, TPayload argument)
{
if (action == null) throw new System.ArgumentNullException("action");
action(argument);
PublishSubscriptionExecutionNotificationEvent(argument, ThreadOption.PublisherThread);
}
/// <summary>
/// called when ever a subscribed event handler is executed. Handled on client side in Event Dashboard module.
/// </summary>
/// <param name="argument"></param>
protected void PublishSubscriptionExecutionNotificationEvent(TPayload argument, ThreadOption threading)
{
try
{
// don't publish notifications for the DashBoard Events!.
if (!(argument is PrismPublishNotificationEventArgs) &&
!(argument is PrismSubscribeNotificationEventArgs) &&
!(argument is PrismSubscriberExecutionNotificationEventArgs) &&
!(argument is PrismUnSubscribeNotificationEventArgs))
{
IEventAggregator eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
var SubscriptionExecutionNotificationEvent =
eventAggregator.GetEvent<PrismSubscriberExecutionNotificationEvent>();
if (SubscriptionExecutionNotificationEvent != null)
{
// Pass the token of the event being published to the subscribingExecutionEvent.
SubscriptionExecutionNotificationEvent.Publish(
new PrismSubscriberExecutionNotificationEventArgs
(targetReference: Action, eventObj: this.EventRef,
subscriptionToken: this.SubscriptionToken.GetHashCode(),
appEventPayLoad: argument, threadingType: threading));
}
}
}
catch (Exception ex)
{
Trace.TraceError("Error occurred during Execution Notification event {0}", ex);
}
}<a id="PublishSubscriptionExecutionNotificationEvent"/>
}
}
In the "DispatcherEventSubscription
" method, the only change is the call
to the "PublishSubscriptionExecutionNotificationEvent
" as shown below in the
"InvokeAction
" method.
"PublishSubscriptionExecutionNotificationEvent
" is in the "EventSubscription
"
class above which serves as a base class to "DispatcherEventSubscription
" and
"BackGroundEventSubscription
" shown below.
/// <summary>
/// Invokes the specified <see cref="System.Action{TPayload}"/>
/// asynchronously in the specified <see cref="Dispatcher"/>.
/// </summary>
/// <param name="action">The action to execute.</param>
/// <param name="argument">The payload to pass
/// <paramref name="action"/> while invoking it.</param>
public override void InvokeAction(Action<TPayload> action, TPayload argument)
{
dispatcher.BeginInvoke(action, argument);
PublishSubscriptionExecutionNotificationEvent(argument, ThreadOption.UIThread);
}
The "PublishSubscriptionExecutionNotificationEvent
" method is located in the
"EventSubscription
" class. This class is the base class for the
"BackgroundEventSubscription
" and the "DispatcherEventSubscription
". Which one
of these is called is determined by the second parameter when initially
subscribing to an event. You can specify whether it runs on the background, UI
or Publisher thread. Depending on which one, this code in the "Subscribe
" method
in "CompositePresentationEvent
" will instantiate the correct subclassed
EventSubscription
:
switch (threadOption)
{
case ThreadOption.PublisherThread:
subscription = new EventSubscription<TPayload>(actionReference, filterReference);
break;
case ThreadOption.BackgroundThread:
subscription = new BackgroundEventSubscription<TPayload>(actionReference, filterReference);
break;
case ThreadOption.UIThread:
subscription = new DispatcherEventSubscription<TPayload>(actionReference, filterReference, UIDispatcher);
break;
default:
subscription = new EventSubscription<TPayload>(actionReference, filterReference);
break;
}
In the "BackgroundEventSubscription
", there is an identical change in the
"InvokeAction
" method:
public override void InvokeAction(Action<TPayload> action, TPayload argument)
{
ThreadPool.QueueUserWorkItem((o) => action(argument));
PublishSubscriptionExecutionNotificationEvent(argument, ThreadOption.BackgroundThread);
}
Notice that the only change between this version and the version in
"DispatcherEventSubscription
" above, is the second parameter in the call to
"PrismSubscriptionExecutionNotificationEvent
". Here is on the background thread,
and in "DispatcherEventSubscription
" it is on the UIThread
.
Remember that when "InternalPublish
" is called from
"CompositePresentationEvent
" as shown above, the "InternalPublish
" method
iterates over the collection of subscriptions to that event being published and
the above "InvokeAction
" is executed for each event handler that is subscribed
to. So when each event handler is being executed, the
PublishSubscriptionExecutionNotificationEvent
is also being published and then
logged in the Event Dashboard.
Using the DashBoard in an Application
To use the new DashBoard view itself in your application involves just a few changes. You need to add the reference to the DashBoard Module.
And you need to create a content control and specify that it use the
region "DashBoardRegion
" as shown below.
Event Dashboard View
The Event Dashboard is a simple view with a backing viewmodel. All of the code for the XAML is basic straightforward XAML that is available in the supplied source. As a summary, here is a screen shot of the PrismEventDashBoardView.xaml.
This view is injected into the Shell.xaml file in the main StockTraderRI
project by using this block of XAML:
<ContentControl x:Name="EventDashBoard" cal:RegionManager.RegionName=
"{x:Static inf:RegionNames.DashBoardRegion}" Grid.Row="3" Margin="10,10,10,10">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Grid>
<Controls:RoundedBox />
<ContentPresenter Content="{TemplateBinding Content}" />
</Grid>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
The above new region, "RegionNames.DashBoardRegion
" establishes where the
dashboard will be injected. Then in the " PrismEventDashBoardView.xaml.cs" file,
the view's class is decorated with the following attributes:
[ViewExport(RegionName = RegionNames.DashBoardRegion)]
[PartCreationPolicy(CreationPolicy.NonShared)]
public partial class PrismEventDashBoardView
The reference to "RegionNames.DashBoardRegion
" is added to the "RegionNames
"
class that is in the StockTraderRI.Infrastructure
project. It's a good idea to
have a similar class to this in your own project to define the various regions
that are used by Prism.
By Exporting the view with the ViewExport
attribute, this is telling Prism
that this view should be cataloged so that it can be later injected into the
region defined above in the Shell.xaml file. This is one of the jobs of
Prism... to maintain a catalog of these exported views and then inject them
into named regions at runtime for final composition of all the parts... pretty
cool.
Basically the view consists of 2 Observable
collections, "EventExecutionSummaryCollection
" and "EventDetailsCollection
". In
the upper right, it uses 4 properties to display the tallies for each of the 4
new Prism Events... Subscriptions
, Publishes
, Executions
and Unsubscribes
. These
properties are incremented as each event is raised by Prism. In the lower right
are the details for the currently selected event in the "Event Executions
Details" grid in the lower left. The "Additions Event Details" it will show
the target class, Module name, location on disk for the DLL, the calling class
and method. On the "Payload Data" tab, it will display the payload data for the
application level event that has executed. Since this payload data can be
substantial, I created a "Refresh" button. Click on this when you need to see
the data for the current executing event. First time around, I was pulling out
the payload as the events loaded, but this actually created memory allocation
errors when using StringBuilder
so I had to go this route. Very reasonable trade
off.
"EventExecutionSummaryCollection
" and "EventDetailsCollection
" are both
Observable collections since these need to be updated in real time as the
application level events are handled by the dash board.
"EventExecutionSummaryCollection
" is a collection of
"EventExecutionSummaryViewModel
".
The
following XAML snippet defines the lower left grid "Event Execution Details".
The "EventDetailsCollection
" is an observable collection of the
"EventDetailsViewModel
" class. The SelectedItem
binding uses the "SelectedEvent
"
property which represents the current instance of the single
"EventDetailsViewModel
" item that is selected in the ListView
.
<ListView x:Name="Events"
Height="300"
ItemsSource="{Binding EventDetailsCollection}"
SelectedItem="{Binding SelectedEvent, Mode=TwoWay}"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Visible">
Then all of the fields that are referenced on the right in the "Additional
Event Details" group box are bound to the "SelectedEvent
" object and one of the
contained properties. For example, the "Module Name" textblock is referenced this
way:
<TextBlock Grid.Column="1"
Grid.Row="1"
TextAlignment="Left"
Text="{Binding SelectedEvent.ModuleName}" />
Event DashBoard ViewModel
The backing view model is where all the work is done. After the view has been
imported into the region, the "SubscribeEvents
" method is called via the
"OnImportsSatisfied
" method. This method is called by the Prism Framework when
the view has been fully loaded. This is made possible by having the viewmodel
inherit from "IPartImportsSatisfiedNotification
" like this:
public class PrismEventDashBoardViewModel : ViewModelBase, IPartImportsSatisfiedNotification
The "SubscribeEvents
" method hooks up the event handlers in this viewmodel to
these events when they are published from Prism Event Aggregator:
private void SubscribeEvents()
{
// Internal Prism Events for notification of Publish/Subscribe calls.
PrismSubscribeNotificationEvent prismSubscribeNotificationEvent =
EventAggregator.GetEvent<PrismSubscribeNotificationEvent>();
prismSubscribeNotificationEvent.Subscribe
(PrismSubscribeNotificationEventHandler, ThreadOption.UIThread, false);
PrismUnSubscribeNotificationEvent prismUnSubscribeNotificationEvent =
EventAggregator.GetEvent<PrismUnSubscribeNotificationEvent>();
prismUnSubscribeNotificationEvent.Subscribe
(PrismUnSubscribeNotificationEventHandler, ThreadOption.UIThread, false);
PrismPublishNotificationEvent prismPublishNotificationEvent =
EventAggregator.GetEvent<PrismPublishNotificationEvent>();
prismPublishNotificationEvent.Subscribe
(PrismPublishNotificationEventHandler, ThreadOption.UIThread, false);
PrismSubscriberExecutionNotificationEvent prismSubscriberExecutionNotificationEvent =
EventAggregator.GetEvent<PrismSubscriberExecutionNotificationEvent>();
prismSubscriberExecutionNotificationEvent.Subscribe(PrismSubscriberExecutionNotificationEventHandler,
ThreadOption.UIThread, false);
}
These are the 4 new events that have been created in the Prism Event Aggregator in the beginning of the article. These events are only raised by the Prism framework itself and not from within your application.
The
"ViewModelBase
" that the PrismEventDashBoardViewModel
inherits from is a basic
view model base class. This serves to provide the reference to the event
aggregator and to implement the "IsActive
" property from the "IActiveAware
"
Prism supplied interface. This is set to true
by the Prism framework when the
view has become active.
using System;
using Microsoft.Practices.Prism;
using Microsoft.Practices.Prism.Events;
using Microsoft.Practices.Prism.ViewModel;
using Microsoft.Practices.ServiceLocation;
namespace StockTraderRI.Modules.EventDashBoard.ViewModels
{
public class ViewModelBase : NotificationObject, IActiveAware
{
private IEventAggregator _eventAggregator;
public event EventHandler IsActiveChanged = delegate { };
public IEventAggregator EventAggregator
{
get { return _eventAggregator ??
(_eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>()); }
}
private bool isActive;
public bool IsActive
{
get { return isActive; }
set
{
if (isActive != value)
{
isActive = value;
if (this.IsActiveChanged != null)
{
IsActiveChanged(this, EventArgs.Empty);
}
}
}
}
}
}
New Prism Event Handlers
The backbone of this view model are the event handlers for the 4 new Prism Events. These are executed when the Prism Event Aggregator is subscribing to an event, publishing an event, executing an event and unsubscribing from an event. Let's take a look at each of them.
The
"PrismSubscribeNotificationEventHandler
" is executed anytime there is a
subscription made in the application level code such as this:
eventAggregator.GetEvent<MarketPricesUpdatedEvent>().Subscribe(MarketPricesUpdated, ThreadOption.UIThread);
Assuming the "args
" parameter is not null
, a new instance of the
"EventDetailViewModel
" is instantiated. This is the backing view for the
information shown in the Event Execution Details grid. There is an enumeration
of "EventTypes
" that is used to specify that this EventType
is of type
Subscription
. Using "GetType()
" on the EventObj
parameter, the name of the Event
is determined. Then using the "TargetReference
" property, the Method Name and
the Target Name of the handler method is returned.
private void PrismSubscribeNotificationEventHandler(PrismSubscribeNotificationEventArgs args)
{
if (args != null)
{
SubscriptionTally++;
EventDetailViewModel detail = new EventDetailViewModel();
detail.EventType = Enums.EventTypes.Subscription;
detail.EventName = args.EventObj.GetType().ToString();
detail.MethodName = args.TargetReference.Method.Name;
detail.TargetName = args.TargetReference.Target != null ?
args.TargetReference.Target.ToString() : "Anonymous";
EventDetailsCollection.Add(detail);
}
}
The Target Name is the name of the class that contains the Method name that
will perform the handling of this event. If an anonymous method is used instead
of a named event handing method, the args.TargetReference.Target
property will
be null
. In this case, the string
"Anonymous
" is returned instead and this will
be displayed as the "Target
" in the Detail tab to the right of the Detail grid.
This new instance of the EventDetailViewModel
is then added to the
ObservableCollection
"EventDetailsCollection
". The "SubscriptionTally
" property
is also incremented so the running total of subscriptions will be displayed in
the upper right panel.
The "PrismPublishNotificationEventHandler
" event handler is executed
whenever your application level code performs a Publish
operation like this:
eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish(CurrentPositionSummaryItem.TickerSymbol);
The same pattern is used here. The "PublishTally
" property is incremented and
a new EventDetailViewModel
instance is instantiated. As shown in the above
listing for the "CompositePresentationEvent
" class, the CallingMethod
and
CallingClass
are determined in the "Publish
" method of
"CompositePresentionEvent
" using the StackTrace
class in the System.Diagnostic
namespace.
private void PrismPublishNotificationEventHandler(PrismPublishNotificationEventArgs args)
{
if (args != null)
{
PublishTally++;
EventDetailViewModel detail = new EventDetailViewModel();
detail.CallingMethod = args.CallingMethod;
detail.CallingClass = args.CallingClass;
detail.EventType = Enums.EventTypes.Publish;
detail.EventName = args.EventObj != null ? args.EventObj.GetType().ToString() : "";
EventDetailsCollection.Add(detail);
}
}
The next handler, "PrismSubscriberExecutionNotificationEventHandler
",
provides the key details in the running of events in your application. As was
shown above, when an event is subscribed to, the "InternalSubscribe
" method in
"EventBase
" is executed by a call from the "Subscribe
" method in the
"CompositePresentationEvent
" class. This "InternalSubscribe
" adds an entry to
the "Subscriptions
" collection as shown here:
protected virtual SubscriptionToken InternalSubscribe
(IEventSubscription eventSubscription, ThreadOption threadingType)
{
if (eventSubscription == null) throw new System.ArgumentNullException("eventSubscription");
eventSubscription.SubscriptionToken = new SubscriptionToken(Unsubscribe);
eventSubscription.EventRef = this;
lock (Subscriptions)
{
Subscriptions.Add(eventSubscription);
}
return eventSubscription.SubscriptionToken;
}
It is this collection of subscriptions for an event that is iterated over
when a "Publish
" event is raised for an event. In the listing above for the
"CompositePresentationEvent
" class, the "Publish
" method first publishes the new
Prism Event "PrismPublishNotificationEvent
". It then calls the "internalPublish
"
event in the "EventBase
" class. Doing it in this order will show the original
application level event and then its subscribers being executed in the "Event
Execution Details" grid in the proper order.
The "InternalPublish
" method in "EventBase
" starts by calling the
"PruneAndReturnStrategies
" method in the same class. This method iterates over
the subscriptions associated with the current event and determines for each
subscription whether or not it should be executed. When subscribing to an event,
there is an optional 4th parameter for a filter expression. You may only want
the subscribed event to be executed under certain circumstances. The
"PruneAndReturnStrategies
" method evaluates this filter and if it's true
, it will
return that subscription instance to the List<>
of events to be executed. Once
this list is created, the "foreach
" loop below will then invoke each handler in
turn. The "executionStrategy
" var
below is a delegate to the event handler.
protected virtual void InternalPublish(params object[] arguments)
{
List<Action<object[]>> executionStrategies = PruneAndReturnStrategies();
foreach (var executionStrategy in executionStrategies)
{
executionStrategy(arguments);
}
}
Each EventSubscription
type (EventSubscription
, BackgroundEventSubscription
,
DispatcherEventSubscription
) has their own version of "InvokeAction
" that
executes the event handler on the proper thread. It then calls the PublishSubscriptionExecutionNotificationEvent
method in the "EventSubscription
" class to publish the new Prism event "PrismSubscriberExecutionNotificationEvent
".
Here is the "InvokeAction
" method for the
"BackGroundEventSubscription
" class. Which one of these versions of
"EventSubscription
" will be used is determined here
in the "Subscribe
" method in the "CompositePresentationEvent
" class.
public override void InvokeAction(Action<TPayload> action, TPayload argument)
{
ThreadPool.QueueUserWorkItem((o) => action(argument));
PublishSubscriptionExecutionNotificationEvent(argument, ThreadOption.BackgroundThread);
}
The handler for the new Prism Event "PrismSubscriberExecutionNotificationEventHandler
"
is shown below. This handler handles the event payload of the original
application level event so that it can be displayed in the "Additional Event
Details" group box to the right of any particular event. Having access to this
original event payload can be very helpful in troubleshooting errors. In order
to display the values of the properties of the event payload, I modified a
version of the "ObjectDumper
" class that is used in the "Official
Visual Studio 2010" samples download. It's modified to
output a string
for all of the class properties so it can be displayed in a textbox
correctly.
When sitting on an Execution Detail record and you click on the "Payload Data" tab, to see the data just click on the "Refresh" button. Getting the payload data only when needed solved some memory issues that occurred when allocating all of the memory for potentially hundreds of events.
The "RefreshEventPayloadCommandExecute
" method is bound to the above
"Refresh" button and simply calls the "ObjectDumper
" utility class. You pass in
the object reference and it traverses the object tree of the class listing out
each property and its value.
private void RefreshEventPayloadCommandExecute()
{
if (SelectedEvent != null && SelectedEvent.Arguments != null)
{
SelectedEvent.PayloadValues = ObjectDumper.Write(SelectedEvent.Arguments, 2);
RaisePropertyChanged(() => SelectedEvent);
}
}
With the parameters that are passed to this event handler, details
such as the application level event handler, the class where it resides and its
location on disk are all known. These details are assigned to a new instance of
the "EventDetailViewModel
" class and added to the "EventDetailsCollection
"
ObservableCollection
.
The "SubscriptionToken
" value, which is just a GUID created when an event is
subscribed to, is used as a key in the "EventExecutionSummaryCollection
". So the
first time a subscription to an event handler is handled by this method, it does
not yet exist in the "EventExecutionSummaryCollection
". So it is added to this
collection and the "ExecuteCounter
" value is set to 1
. If it is executed again,
the Guid lookup will succeed and the counter is incremented. These results are
displayed in the "Event Execution Summary" group box in the upper left of the
dashboard.
/// <summary>
/// Handle when a Publish Event is raised
/// </summary>
/// <param name="args"></param>
private void PrismSubscriberExecutionNotificationEventHandler(PrismSubscriberExecutionNotificationEventArgs args)
{
if (args != null)
{
ExecutionTally++;
EventDetailViewModel detail = new EventDetailViewModel();
detail.Arguments = args.AppEventPayLoad;
detail.EventType = Enums.EventTypes.SubscriberExecution;
detail.SubscriptionToken = args.SubscriptionToken;
detail.EventName = args.EventObj != null ? args.EventObj.GetType().ToString() : "";
detail.MethodName = args.TargetReference.Method.Name;
detail.TargetName = args.TargetReference.Target != null ?
args.TargetReference.Target.ToString() : "Anonymous";
detail.ThreadingType = args.ThreadingType.ToString();
if (args.TargetReference != null && args.TargetReference.Method !=
null && args.TargetReference.Method != null)
{
detail.ModuleName = args.TargetReference.Method.Module.Name;
}
else
{
detail.ModuleName = "";
}
if (args.TargetReference != null && args.TargetReference.Method != null)
{
detail.Location = args.TargetReference.Method.Module.Assembly.Location;
}
else
{
detail.Location = "";
}
detail.SubscriptionToken = args.SubscriptionToken;
EventDetailsCollection.Add(detail);
// if token already exists, increment counter, if not add it...
if (EventExecutionSummaryCollection.Any(EventExecutionSummaryViewModel =>
EventExecutionSummaryViewModel.SubscriptionToken.Equals(args.SubscriptionToken)))
{
EventExecutionSummaryCollection.First(EventExecutionSummaryViewModel =>
EventExecutionSummaryViewModel.SubscriptionToken.Equals(args.SubscriptionToken)).Increment();
}
else
{
EventExecutionSummaryViewModel newSummary = new EventExecutionSummaryViewModel();
newSummary.SubscriptionToken = args.SubscriptionToken;
newSummary.MethodName = detail.MethodName;
newSummary.TargetName = detail.TargetName;
newSummary.ExecuteCounter = 1;
EventExecutionSummaryCollection.Add(newSummary);
}
}
}
private void PrismUnSubscribeNotificationEventHandler(PrismUnSubscribeNotificationEventArgs args)
{
if (args != null)
{
UnSubscribeTally++;
EventDetailViewModel detail = new EventDetailViewModel();
detail.CallingMethod = args.CallingMethod;
detail.CallingClass = args.CallingClass;
detail.EventType = Enums.EventTypes.UnSubscribe;
detail.EventName = args.EventObj.GetType().ToString();
EventDetailsCollection.Add(detail);
}
}
Enhancement Ideas
For the purposes of this article, I created it to just use native WPF controls and to be as straightforward as possible. But there are ways to jazz it up a bit more.- I recommend using the XCeed Extended WPF Toolkit Community Edition. It
can be downloaded here on
CodePlex. I've used it on another WPF project and it works great. It's free
and contained in one DLL that you can drop in and start using very quickly.
Great suite of controls. You could replace the standard listview control with
their
datagrid
and get a lot more functionality. - For a nicely enhanced tab control, try FabTab. Another very easy to use control with lots of nice tab options. One nice feature is hovering over an unselected tab and seeing a preview of the tab.
- To make this more graphical, you could replace the counters for example with more dashboard type controls. A pretty nice suite of dashboard controls can be found here on Codeplex. Contains lots of controls with all source. Attractive and easy to use.
Utilities
A very handy utility for packaging up a project to send it to someone via email or for use in a CodeProject article like this is the CleanProject utility on MSDN. This will start in the home project of the project and delete all of the files in the bin and obj folders and other temporary files and then zip them up in a clean zip file. It has reduced the file size by half when I 've used it.
Another useful too is Reference Assistance. It can found here on Codeplex. This will remove any unneeded project references. Just Expand the "References" section and right-click and choose "Remove Unused References". It will determine which ones are needed and delete the rest. Works great.
In Summary...
Through the use of four new Prism events, that Prism
itself publishes in response to your application level publishing of custom
events, this Event Dashboard utility is able to provide you with detailed
information on your entire application. It requires no change in your existing
code and no change in how you write new code. The Prism framework is the single
pipe line for all eventing operations. It is here that these new events are
raised so that the dashboard view model can listen for them and properly handle
them to update the dash boards statistics, summary and detailed information on
each Subscribe
, Publish
, Execution
and UnSubscribe
event that takes place.
History
- 03/12/2014 - First version published