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.

Here is the subscription in the CustomerOrders module:

  _eventAggregator.GetEvent<CustomerSelectedEvent>()
.Subscribe(NotifyView);

But the subscription is never raised and NotifyView is never called! The CustomerOrders module is instantiated alright but it never shows a Customer.

Maybe I didn't publish correctly. So I try subscribing inside some other class that I'm sure is in-use ... such as a class in CustomerSearch itself.


  _eventAggregator.GetEvent<CustomerSelectedEvent>()
.Subscribe(HeardIt);


...
public void HeardIt(object dummy) {
var sink = "I heard you";
}

Put a breakpoint on HeardIt; works like a charm.

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.

  const bool keepAlive = true;
_eventAggregator.GetEvent<CustomerSelectedEvent>()
.Subscribe(NotifyView, ThreadOption.UIThread, keepAlive);

It works!

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:


  // Destructor to demo when GC'd
~MyModule() {
System.Console.WriteLine("Goodbye, MyModule");
}

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.

13 comments:

Jeremy Miller said...

Ward,

This is simple if you instead have the event handler spun up and controlled by your IoC container. The "Module" really just becomes a mechanism to add things into the IoC container.

Jeremy

Alana said...

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.


Maria

http://memory1gb.com

Jon said...

I think a solution to this would be to pass the module-object to the constructor of the view. This way the module won't be GC'd until the view does - like this:

//This is the constructor of the
//Customerorders module
public CustomerOrdersModule()
{
...
...
var view = new CustomerOrdersView(this);
...
...
}


I might be totally off here, but anyway... ;o)


Jon

surexxx said...

Thanx a lot, I had the same problem!

joekannapat said...

Had the exact same issue too and your post helped me get the insight into the issue. Thanks for the good post

DT said...

Excellent article. Your hair pulling has saved me from having to do the same!

Anonymous said...

[b]Rino 530HCx[/b]

* Рация 5Вт (GMRS, до 15 км. при прямой видимости)
* Передача своих координат Peer-to-Peer Positioning
* GPS навигатор (приемник SirfStarIII)
* Барометрический высотомер
* NOAA погодное радио
* Электронный компас
* Картография

Навигатор с рацией Rino 530HCx является обновлением в серии Garmin Rino. Похожие на носорога (отсюда и название), эти водозащищенные (IPX 7) приборы уникальны в своем классе. Благодаря функции Peer-to-Peer Positioning Вы сможете передать свои точные координаты своему напарнику (или друзьям) и они увидят на дисплее своего Rino на каком расстоянии Вы от них находитесь и в каком направлении им нужно двигаться чтобы встретиться с Вами. Отныне вы можете не покупать навигатор и рацию, а выбрать Garmin Rino 530HCx. Внутри водонепроницаемого корпуса расположены FRS/GMRS радио мощностью 5Вт, GPS навигатор , высотомер, электронный компас, NOAA погодное радио и слот для MicroSD карты.

[url=http://info.je1.ru/GPS_016.html]Подробнее...[/url]

Tim McCurdy said...

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.

Anonymous said...

[b]Ультропрозрачная защитная пленка для GPS навигатора на экран 4.3[/b]

[url=http://info.je1.ru/GPS_084.html]Подробнее...[/url]

Brownie said...

I think a simple solution is to have a DataTrigger for the View set Visibility=Collapsed when the "CurrentCustomer" value is Null (or if you're using VSM do the similar actions in there).

This way your CustomerOrders view can be created and act as the subscriber to the event (giving a strong root).

Ward Bell said...

It's been awhile since I looked at this.

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

Of course I much appreciate your thoughts on that tangential subject.

Brownie said...

Ahhh....didn't realize it was a tabcontrol that you were working with. Thought it was just a region in your view. I guess it'll help next time for me to read before commenting ;)

Love the blog and your work!

Anonymous said...

I spend hours trying to get some event subscriptions firing and what solved it for me was to add the EventAggregator instance to ServiceLocator during bootstrapping.

I placed it within overloaded Container method because in my app it needs the Logger instantiated first...

protected override Microsoft.Practices.Prism.Regions.RegionAdapterMappings ConfigureRegionAdapterMappings()
{

// container should exist by now so register event aggregator instance
this.Container.RegisterType(new ContainerControlledLifetimeManager());
this.Container.RegisterInstance(_eventAggregator);


RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings();

if (mappings != null)
{
mappings.RegisterMapping(typeof(DockingManager), this.Container.TryResolve());
}

return mappings;
}

That did the trick.