Saturday, May 2, 2009

Prism Event Aggregator Subscription Blues

After too many hours of debugging and hair-pulling, I finally figured out why my Prism EventAggregator (EA) subscriptions were not working in an important case. The reason made perfect sense ... once understood.

Here is the setup.

I have a CustomerOrders module to manage viewing and editing of one Customer and its Orders.

I have another module, CustomerSearch, in which the user searches for Customers.

The two modules are de-coupled.

The CustomerOrders module learns about which Customer to show when CustomerSearch publishes the SelectedCustomerEvent.

The CustomerOrders module will not display itself until it hears the first SelectedCustomerEvent.

This is a canonical example of cross-module Eventing, perfect for Prism's EventAggregator.

I break at the point where I'm adding subscriptions to CustomerSelectedEvent; they are all there! I can see both subscriptions in the CustomerSelectedEvent's list of subscriptions.

After a few frustrating hours, I happen to look at _eventAggregator subscriptions when HeardIt is called. Now there is only one, the one for HeardIt. The CustomerOrders subscription is gone!

Then I remember, EventAggregator holds weak references to subscriptions by default ... so the subscriber doesn't have to unsubscribe when it is disposed or garbage collected (GC'd). This is a very cool feature. Sadly, I immediately suspect that this is the source of my problem. To test that thesis, I force the subscription to use strong references.

Of course now the instance in which I make this subscription will hang around for the life of the application (the life of the EA to be precise). This is a potential memory leak. If I'm going to make and forget a lot of these instances, I better remember to unsubscribe, perhaps via IDisposable. That doesn't seem like fun.

Why did the subscriber disappear ... taking its subscription with it?

Prism decoupling was just doing its job. Most Prism modules that you will ever create actually disappear rather quickly.

You can verify that thesis. Drop the following in your module class (the inheritor of IModule) and set a breakpoint:

If your module class is very simple as it should be ... perhaps some type registrations before dropping a view into a region ... you'll see that destructor called in no time; happens almost immediately for me because I'm running in a VM where the garbage collector is very busy.

So my subscription disappeared because I subscribed within a class that itself disappears ... really quickly.

In my example, I was unable to find or construct an instance of a class that outlived the module.

I could have put it inside the View (the ViewModel to be precise); once the View was injected into the visual tree, it would outlive the module because the visual tree would keep it alive. That is why the subscription to "HeardIt" worked in the CustomerSearch module . I had subscribed inside a ViewModel after it's companion View had been presented. It didn't matter that the CustomerSearch module class instance, which had created that ViewModel, had long since been GC'd.

Unfortunately, I can't follow that example in the CustomerOrders module. Can you see why?

Remember I said at the beginning that the CustomerOrders module waits for the first publication of CustomerSelectedEvent before it shows itself. If I don't show anything, everything I create in that module evaporates (get's GC'd) before the first publication of CustomerSelectedEvent!

I have to do something to keep the module around until it can do its work. I'm sure you can think of plenty of ways; I did. They're mostly ugly. I decided that I should put my solution near the cause of the problem ... and so I ensure that at least one subscription has "keepAlive = true".

I won't worry about the potential memory leak from hanging on to the module class; I don't expect to have more than one instance of this module in the lifetime of this application. I'll just document the issue and move on.

Hope this helps you!

p.s.: No ... I did not actually put this logic in the module class. Module classes are supposed to be bare bones. I put it in a Coordinator class, an instance of which is resolved by the CustomerOrders module. The problem is the same. The coordinator is referenced only by the module so it evaporates when the module does. I thought this detail would only interfere with exposition were it introduced earlier.

I recently came across your blog and have been reading along. I thought I would leave my first comment. I dont know what to say except that I have enjoyed reading. Nice blog. I will keep visiting this blog very often.

Hey Ward, actually your problem (and everyone else's on the internet using Prism) is that you're using the IModule to load/inject views into Regions. I noticed this flaw in CAB/Prism 7 years ago when it first came out and didn't like it. We always talk about seperation of concerns but somehow we think it's fine for an external module to "know" everything about the Shell? How does a Module know if a Region exists in the Shell or not!? That's why I created a very simple Xml file that is a companion to the ModuleCatalog and tells the shell which Views/Regions to create...and when! This allows the Modules to load on demand and I have absolutely zero code in the IModules injection views into the Shell. It's great! Now of course, the Module can still do that just by Publishing a new ShellViewRegistrationEvent. But this is all another story...

Anyway, my solution to this whole ordeal is to register a ViewModel that is not attached to any UI but add it into the container as a "static" instance (container.RegisterInstance(..., new ContainerControlledLifetimeManager()). Then inside that ViewModel it listens for the events. Since there's only ever one instance of it I shouldn't need to worry about memory leaks. Also, I need it for the life of the application so it makes sense.

@Jeremy_Miller's proposal is a good one... although you have to know your IoC container well enough to make it do the event autowiring.

Many of the suggestions for how to launch the view show promise. I don't quite see how Brownie's approach will work because I don't know how to keep the tab out of the TabControl by controlling the visibility of the view in the tab.

I want to remind everyone that the real issue in this post was weak event handlers in Prism.

How I fell into the trap ... my story about waiting for a Customer selection before adding the tab ... all of that was incidental.

About Me

Ward is a Microsoft MVP and the V.P. of Technology at IdeaBlade (www.ideablade.com), a software consultancy and the makers of the "Breeze JS" and "DevForce" client data management libraries for JavaScript and .NET application development.
Ward often obsesses on client technologies for business applications, data access, and development practices.