Contents
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.
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.
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. :)
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 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.
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 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.
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
{
public class PrismSubscribeNotificationEvent : CompositePresentationEvent<PrismSubscribeNotificationEventArgs>
{
}
public class PrismSubscribeNotificationEventArgs : EventArgs
{
public object EventObj { get; set; }
public object AppEventPayLoad { get; set; }
public int SubscriptionToken { get; set; }
public Delegate TargetReference { get; set; }
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
{
public class PrismPublishNotificationEvent : CompositePresentationEvent<PrismPublishNotificationEventArgs>
{
}
public class PrismPublishNotificationEventArgs : EventArgs
{
public object EventObj { get; set; }
public object AppEventPayLoad { get; set; }
public string CallingClass { get; set; }
public string CallingMethod { get; set; }
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
{
public class PrismSubscriberExecutionNotificationEvent :
CompositePresentationEvent<PrismSubscriberExecutionNotificationEventArgs>
{
}
public class PrismSubscriberExecutionNotificationEventArgs : EventArgs
{
public object EventObj { get; set; }
public object AppEventPayLoad { get; set; }
public int SubscriptionToken { get; set; }
public Delegate TargetReference { get; set; }
public ThreadOption ThreadingType { get; set; }
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
{
public class PrismUnSubscribeNotificationEvent :
CompositePresentationEvent<PrismUnSubscribeNotificationEventArgs>
{
}
public class PrismUnSubscribeNotificationEventArgs : EventArgs
{
public object EventObj { get; set; }
public string CallingClass { get; set; }
public string CallingMethod { get; set; }
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
the eventAggregator
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 the Publish
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.
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
{
public class CompositePresentationEvent<TPayload> : EventBase
{
private IDispatcherFacade uiDispatcher;
private IEventAggregator eventAggregator;
private IDispatcherFacade UIDispatcher
{
get
{
if (uiDispatcher == null)
{
this.uiDispatcher = new DefaultDispatcher();
}
return uiDispatcher;
}
}
public CompositePresentationEvent()
{
try
{
eventAggregator = ServiceLocator.Current.GetInstance<IEventAggregator>();
}
catch (Exception ex)
{
Trace.TraceError("Error occurred getting event aggregator: {0}", ex);
}
}
public SubscriptionToken Subscribe(Action<TPayload> action)
{
return Subscribe(action, ThreadOption.PublisherThread);
}
public SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption)
{
return Subscribe(action, threadOption, false);
}
public SubscriptionToken Subscribe(Action<TPayload> action, bool keepSubscriberReferenceAlive)
{
return Subscribe(action, ThreadOption.PublisherThread, keepSubscriberReferenceAlive);
}
public SubscriptionToken Subscribe(Action<TPayload> action,
ThreadOption threadOption, bool keepSubscriberReferenceAlive)
{
return Subscribe(action, threadOption, keepSubscriberReferenceAlive, null);
}
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);
if (eventAggregator != null && !(this is PrismUnSubscribeNotificationEvent)
&& !(this is PrismSubscribeNotificationEvent) && !(this is PrismPublishNotificationEvent)
&& !(this is PrismSubscriberExecutionNotificationEvent))
{
var internalSubscribeNotificationEvent = eventAggregator.GetEvent<PrismSubscribeNotificationEvent>();
if (internalSubscribeNotificationEvent != null)
{
internalSubscribeNotificationEvent.InternalPublish(new PrismSubscribeNotificationEventArgs
(subscriptionToken: token.GetHashCode(), targetReference: subscription.Action, eventObj: this));
}
}
return token;
}
public virtual void Publish(TPayload payload)
{
try
{
if (eventAggregator != null && !(this is PrismUnSubscribeNotificationEvent)
&& !(this is PrismSubscribeNotificationEvent) && !(this is PrismPublishNotificationEvent)
&& !(this is PrismSubscriberExecutionNotificationEvent))
{
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;
}
var internalPublishNotificationEvent = eventAggregator.GetEvent<PrismPublishNotificationEvent>();
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);
}
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))
{
StackTrace stackTrace = new StackTrace();
string callingClass = stackTrace.GetFrame(1).GetMethod().DeclaringType.FullName;
string callingMethodName = stackTrace.GetFrame(1).GetMethod().Name;
var internalUnSubscribeNotificationEvent = eventAggregator.GetEvent<PrismUnSubscribeNotificationEvent>();
if (internalUnSubscribeNotificationEvent != null)
{
internalUnSubscribeNotificationEvent.InternalPublish
(new PrismUnSubscribeNotificationEventArgs(eventObj: this,
callingClass: callingClass, callingMethod: callingMethodName));
}
}
}
}
}
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
{
public interface IEventSubscription
{
SubscriptionToken SubscriptionToken { get; set; }
EventBase EventRef { get; set; }
[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.
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
{
public class EventSubscription<TPayload> : IEventSubscription
{
private readonly IDelegateReference _actionReference;
private readonly IDelegateReference _filterReference;
public EventBase EventRef { get; set; }
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;
}
public Action<TPayload> Action
{
get { return (Action<TPayload>)_actionReference.Target; }
}
public Predicate<TPayload> Filter
{
get { return (Predicate<TPayload>)_filterReference.Target; }
}
public SubscriptionToken SubscriptionToken { get; set; }
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;
}
public virtual void InvokeAction(Action<TPayload> action, TPayload argument)
{
if (action == null) throw new System.ArgumentNullException("action");
action(argument);
PublishSubscriptionExecutionNotificationEvent(argument, ThreadOption.PublisherThread);
}
protected void PublishSubscriptionExecutionNotificationEvent(TPayload argument, ThreadOption threading)
{
try
{
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)
{
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.
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.
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.
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()
{
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.
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 (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
I have been a software engineer for the past 30 years. I specialize in C#/WPF development. I have been a speaker at MS conferences and user groups. I also wrote a book "WebRad: Building data driven websites with Visual FoxPro".