RegionMemberLifetime KeepAlive = false and View Injection, view does not show up

Topics: Prism v4 - WPF 4
Sep 27, 2012 at 6:58 PM
  • WPF
  • MEF
  • Many nested views using scoped regions
  • View Injection only, no INavaigation

I'm starting to implement RegionMemberLifetime to help destroy views when they are removed from a region.

The problem I'm having is when I apply the attribute to a view or it's viewModel, the view will not show up.  My suspicion is there ends up being no reference to it after injecting the view, so it gets cleaned up right away.

Example:

There is an existing shell and main tab control as a target region.
User clicks a button in the shell to open a new tab for Patient Details (injected view all using scoped regions), after injecting the Patient View the Patient module raises the event OnParentWorkspaceInjected (event payload contains the scoped region used to inject the Patient View).

Other modules subscribe to the OnParentWorkspaceInjected event and inject their own view into a region on the PatientView called PatientDetailModules.

There are several modules that inject their views, all done using this appraoch:

View

  [Export]
  [PartCreationPolicy(System.ComponentModel.Composition.CreationPolicy.NonShared)]
  [RegionMemberLifetime(KeepAlive = false)]
  public partial class LoanView : UserControl
  {

...

In the controller class in the module for that view

private void OnParentWorkspaceInjected(PatientWorkspaceInjectedEvent viewEvent)
{
  LoanView view = this.serviceLocator.GetInstance<LoanView>();
  IRegionManager scopedParentRegion = viewEvent.ScopedRegionManager;
  IRegion region = scopedParentRegion.Regions[RegionNames.PatientDetailModules];
  IRegionManager scopedRegion = region.Add(view, null, true);

  view.ViewModel.SessionKey = viewEvent.SessionKey;
  view.ViewModel.UniqueKey = viewEvent.UniqueKey;
  view.ViewModel.ScopedRegionManager = scopedRegion;
  region.Activate(view);

  Guid uniqueKey = Guid.NewGuid();
  this.SetView(uniqueKey, view);
  view.ViewModel.UniqueKey = uniqueKey;

  view.ViewModel.RaisePublishGetList();
}

The only code change I made was adding [RegionMemberLifetime(KeepAlive = false)] to the view.

If I add that attribute to either the view or viewModel, the view will not show up.

If that attribute isn't used the views show up and behave as expected.

Thanks in advance.

Developer
Sep 28, 2012 at 6:45 PM

Hi,

We have tried setting up a spike to analyze this scenario, but so far we were unable to reproduce this behavior.

It would be helpful if you could provide us with a repro-sample application portraying this problem, so that we can analyze it in further detail and help you find the cause behind this behavior.

Regards,

Damian Cherubini
http://blogs.southworks.net/dcherubini

Oct 6, 2012 at 4:00 PM
Edited Oct 6, 2012 at 4:01 PM

Hi,

Thanks for looking into this.

I've created an example solution that is based on our real solution.  The example uses the same event and injection patterns but is a dramatically reduced and simplified code example.  Some things in the example may not make sense but are there because they match our actual code.

At this point I cannot reproduce the missing view, but I can reproduce something else I was running into before the views stopped showing up.

I could decorate views/viewmodels with the KeepAlive=false but when trying to remove the view but would get this error The region does not contain the specified view.

If you take the KeepAlive attribute off that view the removal works as expected.  The weird thing is, stepping through the code you can see the view object in the regionManager.Views collection.  I'm thinking whatever is causing this problem may be related to the other missing view problem, so If one problem can be solved it may resolve them all.  In other words, I have never been able to get KeepAlive=false to work.

Example Solution Instructions

  • Download from my skydrive here http://sdrv.ms/TdOYpr
  • Build and run
  • To reproduce the error
    • Click ok on login screen (no username/pwd needed)
    • Click "Customer: Injected via View Discovery" button to inject a customer workspace
    • Click "Raise Event" button in the Orders View
    • Click "Remove View" button in the Order Details View
    • Get the The region does not contain the specified view error
  • To run without the error
    • Edit OrderDetailsView.xaml.cs and remove the [RegionMemberLifetime(KeepAlive = false)] attribute
    • Run and follow the steps above and the view will remove successfully

The solution is using Prism 4.1 and Visual Studio 2010.

I will continue to try and reproduce the scenario where the view won't show at all as stated in my opening post.

I really appreciate the help.

Cheers

Oct 9, 2012 at 3:14 PM

I think I've resolved the second issue noted in my last post.  I was still using region.Remove(view) but should have been using region.Deactivate(view) in combination with KeepAlive=false.

Related discussion in this post: http://compositewpf.codeplex.com/discussions/272855

I still cannot reproduce in the example solution the views not showing up but am continuing to work on that as it is still a problem in our main solution.

Oct 9, 2012 at 3:56 PM
Edited Oct 9, 2012 at 3:57 PM

Another related problem.

Now that I'm deactivating views, the views are being removed fine it seems.  On each test of injection/deactivation there seems to only be 1 view in the region collection which gets deactivated leaving no views in the collection.

But, the view models are not getting destroyed.

I set KeepAlive=false on the view models as well.

I'm testing if the viewModels are still in memory or not by subscribing to the event raised on the home page.  If I see output in the console I believe that viewModel to still be active despite it's view having been deactivated and removed from the region collection.

If I inject/deactivate only one OrderDetails view, the viewModel will not pick up the event.

If I inject/deactivate more than one, I will see output from the ViewModel for the event, but never for the first viewModel.

In other words, I always see events handled for the total number of views injected minus 1.

I tried hacking in some cleanup code to both un-subscribe from the event as well as setting the view's datacontext to null hoping to drop references, but no change.

Are the viewModels just not getting garbage collected yet or is something else keeping them around? I don't make any other references to them as far as I know.  But I've never been able to get this part of Prism working correctly so may be missing something...

Developer
Oct 9, 2012 at 8:59 PM

Hi,

First, regarding your problem when removing views, it seems that (as you mentioned) it was related to a known issue in Prism when removing a view with KeepAlive set as false using the Remove method:

In the aforementioned work item you can find another approach to workaround this problem by modifying the RegionMemberLifetimeBehavior class.

Second, regarding the issue you are mentioning about your view models not being destroyed, it seems to be that the reason behind it was that the view models were ready to be garbage collected but the garbage collector was not run. You can check this, for example, by adding the following line to manually run the garbage collector before publishing the HomePageButtonClickEvent:

GC.Collect();

After doing so, you should see that only one OrderDetailsViewModel responds to the HomePageButtonClickEvent: the view model of the current OrderDetailsView. The previous view models are effectively being garbage collected.

I hope this helps,

Damian Cherubini
http://blogs.southworks.net/dcherubini

Oct 10, 2012 at 4:03 PM

I'm thinking that is a problem then if viewModels will linger with valid event subscriptions.  What would be the best practice to ensure my "removed" viewModels won't interfere with executing code, implement IDisposable and use a region extension method?  I definitely don't want "removed" viewModels to continue to handle events.

Regarding my original problem of missing views I have finally been able to reproduce it.
I've uploaded a new solution to the same location here:  http://sdrv.ms/TdOYpr see Example2 

The problem seems to be related to injecting multiple views into a tab control region.  Here's the combination of problems:

KeepAlive=false is being added to the views only for this testing:

When both modules show up, Orders is first, History is second from left to right.

  • Mark no modules as KeepAlive=false, both Orders and History show up
  • Mark both modules as KeepAlive=false, only History shows up
  • Mark only History KeepAlive=false, both Orders and History show up
  • Mark only Orders KeepAlive=false, only History shows up

This is similar to behaviour I see in our main solution.  I've experimented with ViewSortHint but didn't come up anything conclusive.

I don't have this problem in other areas of our solution, I can apply KeepAlive=false and the views show up as expected.  So far, the only time I have this problem is with the structure of nested views and injecting to the tab control as demonstrated in the Example solution.

Developer
Oct 10, 2012 at 8:56 PM

Hi,

In the same scenario that your repro-sample application, where the OrderDetailsView will be always removed when deactivated, a possible approach could be to unsubscribe from the HomePageButtonClickEvent when the view is deactivated. For doing so, you could take advantage of the IActiveAware interface, which defines an IsActive property. When the state of the view changes, Prism change the value of this property to inform the view of its current state. Therefore, in the setter of this property, you could check if the value is false and invoke the Cleanup method to unsubscribe from the event:

private bool isActive;

public bool IsActive 
{
    get
    {
        return isActive;
    }
    set
    {
        this.isActive = value;

        if (this.IsActive == false)
        {
            this.Cleanup();
        }

        this.RaisePropertyChanged("IsActive");
    }
}
Also, in order for this to work, you will need to subscribe / unsubscribe passing the method directly instead of a lambda expression (when using lambda expressions, the lambda passed to the Subscribe method will be different from the one passed to the Unsubscribe method and hence, the un-subscription fails:)

this.eventManager.Subscribe<HomePageButtonClickEvent>(this.OnHomePageButtonClick);
this.eventManager.Unsubscribe<HomePageButtonClickEvent>(this.OnHomePageButtonClick);

Regarding your original problem with the OrdersView and HistoryView, as far as I know, this is a normal behavior when using a TabControl as a region. Based on my understanding, a "TabRegion" only has one active view: the currently selected tab. All the other views in the "TabRegion" are considered deactivated.

Basically, after injecting the OrdersView you are injecting and activating the HistoryView. Hence, as only one view can be active, the OrdersView is deactivated. As the view is marked with KeepAlive set to false, Prism removes it from the region.

You can check this behavior in with other TabControls. For example, if you set KeepAlive to be false in the HomePageView, when the CustomerWorkspaceView is injected the HomePageView is also removed.

I hope you find this useful,

Damian Cherubini
http://blogs.southworks.net/dcherubini

Oct 11, 2012 at 10:45 PM

Thanks for all the info so far, this is helping narrow down what we need to at least.

I'd appreciate your opinions on these approaches:

 

Using KeepAlive=False


I don't think we're going to use this approach at all since it doesn't work in our tabbed region scenarios.

I'd also rather not have multiple patterns to support or chose from especially when one simply won't work well if they get mixed up.

So, I'm thinking to not use the KeepAlive=false pattern and continue to use region.Remove instead of region.Deactivate.

My question there is what happens with the view/viewModel and garbage collection?  If the view is removed, is the viewModel flagged for GC like it is when using the KeepAlive=false/region.Deactivate combination?  Or do I need to do something different to deal with the viewModel if .remove(view) only removes the view?

Do you have suggestions on a best practice for cleaning up views/viewModels when not using KeepAlive=false?

 

IActiveAware and Cleanup


I can see where implementing IActiveAware will be beneficial, but again since it is called all the time when a tabbed view isn't active I can't use it for viewModel cleanup since the view will be used again once the user clicks on it.

That leads me to think I need to use an extension method (as I believe you've mentioned before in other posts) to help control when view/viewModels are destroyed and cleaned up in a consistent fashion.

Something like:

region.RemoveAndCleanup(view)
{ view.ViewModel.Cleanup() this.remove(view)
}

Thoughts on that?

 

I'll continue experimenting in the meantime.

Regards.

Oct 12, 2012 at 3:20 PM

Hi Again,

I think I've got this all figured out and mostly tested.

We won't be using KeepAlive=false and instead will use region.Remove().  This does flag viewModels for cleanup which I can prove the same way as done above calling GC.Collect() to see the events no longer subscribing.

The next related step is to upgrade the solution to Prism 4.1 to take advantage of the ClearChildViewsRegionBehavior to ensure nested views/viewModels are also removed properly.

We can use the region extension method pattern if we need special cleanup routines but for now won't do anything different.

So after a lot of effort, trial and error, we're basically changing nothing but I at least understand how things work much better :)

Thanks again for all the help and if you have any additional comments or opinions please share.

Cheers

Developer
Oct 12, 2012 at 5:47 PM

Hi,

I am glad you found this useful.

As a side note, I would like to comment that Prism does not only provide the RegionMemberLifetimeAttribute, it also provides an IRegionMemberLifetime interface. The RegionMemberLifetimeBehavior (the one in charge of removing the deactivated views) checks if the view / view model implements either the attribute or the interface to know if the view should be removed or not. The main difference between using the attribute or the interface is that the interface allows you to change the value of the KeepAlive property during run-time, while the attribute defines the KeepAlive value in design-time.

Hence, when using the interface instead of the attribute you can change the KeepAlive property of a view, for example, if the view is related to the information being shown to the user or not (for example a details view), if a view contains modified data that need to be saved or not (for example an edit view), if the region where the view was injected can contain deactivated views or not (for example a TabControl,) etc. Therefore, the IRegionMemberLifetime interface can provide you with more flexibility than the attribute, which could be useful in some scenarios.

Regards,

Damian Cherubini
http://blogs.southworks.net/dcherubini