8

We started a WPF project, with Prism, and I've come to a question:

Sometimes in the ViewModel, we register some events or started services that we have to stop before closing. It means that when I close the application, I need to release the resources I acquired within the ViewModel. The Dispose would then make a lot of senses.

Currently I'm using the ViewModelLocator.Autowire = True of Prism, and I was thinking that when the View was not required anymore, it would dispose it if needed.

I've two cases:

  • When I "Navigate" to a view(RegionManager.RequestNavigate("RegionName", "RegionUri"))
  • When I've a "subview"(which is an UserControl with its own ViewModel) used in a View

My question is: What is the correct approach to dispose those ViewModel? I can see multiple way of doing it, but I'm not sure which one is the correct one.

Tseng
  • 61,549
  • 15
  • 193
  • 205
J4N
  • 19,480
  • 39
  • 187
  • 340
  • As for event registration, do you mean the .NET `event` events? You shouldn't really use them in MVVM, rather use a pub/sub system like Prism's PubSubEvent package to have loosely coupled messaging between the viewmodels. They are implemented as weak events and won't block garbage collection – Tseng Jun 09 '16 at 09:14
  • @Tseng I mean I've some service on which I can register some services. – J4N Jun 09 '16 at 11:48
  • @Tseng: pub/sub system leads to another layer of abstraction, in this case with unclear benefits, but makes refactoring, code navigation (find all references, go to definition) and debugging harder. Why should be viewmodels different from any other .net classes? – Liero Jun 09 '16 at 12:26
  • I said it already, using Pub/Sub you don't have back references which prevents the subscribers from being garbage collected, hence you don't have to unregister before the ViewModel is disposed. And you don't need to retrieve an instance of the service for the sole purpose of registering to it. Just register to the event and fire it, no need to care who or if someone receives it – Tseng Jun 09 '16 at 13:15

2 Answers2

4

I general, you should do your cleanup login in Unloaded event.

I solved this by calling Activate and Deactivate methods on my ViewModel from View's Loaded resp Unloaded event.

interface IViewModelLifeCycle
{
   void Activate();
   void Deactivate();
}

public class MyComponentViewModel: BindableBase, IViewModelLifeCycle {
   public void Activate(){}
   public void Deactivate()
}

This is basically the same principle as in Brian's answer, which I like and upvoted. This is just more generic and you are not tied to RegionManager (I don't like RegionManager)

[Optional]

in order to make it more comfortable, I have created behavior attached to view, instead of writing some code behind code:

<local:MyComponentView DataContext="{Binding MyComponentViewModel}"
                       local:ViewModelLifeCycleBehavior.ActivateOnLoad="True" />


<Style x:Key="PageStyle" TargetType="Page">
    <Setter Property="local:ViewModelLifeCycleBehavior.ActivateOnLoad" Value="True" />
</Style>

The behavior implementation is a bit chatty, but it's actually very simple pattern. In PropertyChanged callback, attach to FrameworkElements events.

public static class ViewModelLifeCycleBehavior
{
    public static readonly DependencyProperty ActivateOnLoadProperty = DependencyProperty.RegisterAttached("ActivateOnLoad", typeof (bool), typeof (ViewModelLifeCycleBehavior),
        new PropertyMetadata(ActivateOnLoadPropertyChanged));

    public static void SetActivateOnLoad(FrameworkElement element, bool value)
    {
        element.SetValue(ActivateOnLoadProperty, value);
    }

    public static bool GetActivateOnLoad(FrameworkElement element)
    {
        return (bool)element.GetValue(ActivateOnLoadProperty);
    }


    private static void ActivateOnLoadPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    { 
        if (DesignerProperties.GetIsInDesignMode(obj)) return;
        var element = (FrameworkElement)obj;

        element.Loaded -= ElementLoaded;
        element.Unloaded -= ElementUnloaded;

        if ((bool) args.NewValue == true)
        {
            element.Loaded += ElementLoaded;
            element.Unloaded += ElementUnloaded;
        }
    }



    static void ElementLoaded(object sender, RoutedEventArgs e)
    {
        var element = (FrameworkElement) sender;
        var viewModel = (IViewModelLifeCycle) element.DataContext;
        if (viewModel == null)
        {
            DependencyPropertyChangedEventHandler dataContextChanged = null;
            dataContextChanged = (o, _e) =>
            {
                ElementLoaded(sender, e);
                element.DataContextChanged -= dataContextChanged;
            };
            element.DataContextChanged += dataContextChanged;
        }
        else if (element.ActualHeight > 0 && element.ActualWidth > 0) //to avoid activating twice since loaded event is called twice on TabItems' subtrees
        {
            viewModel.Activate(null);
        } 
    }

    private static void ElementUnloaded(object sender, RoutedEventArgs e)
    {
        var element = (FrameworkElement)sender;
        var viewModel = (IViewModelLifeCycle)element.DataContext;
        viewModel.Deactivate();
    }


}
Community
  • 1
  • 1
Liero
  • 25,216
  • 29
  • 151
  • 297
  • Thank you for your answer, but my DataContext is set automatically by Prism's ViewModelLocator(cf http://brianlagunas.com/getting-started-prisms-new-viewmodellocator/ ), So unfortunately I don't have at design time the possibility to set something from the XAML to the ViewModel. How would you proceed? – J4N Jun 09 '16 at 11:46
  • It does not matter where you set datacontext. I guess you have something like `prism:ViewModelLocator.AutoWireViewModel=”True”`. Just add `local:ViewModelLifeCycleBehavior.ActivateOnLoad="True"` next to it. – Liero Jun 09 '16 at 12:19
  • 1
    From MSDN: "Note that the Unloaded event is not raised after an application begins shutting down... If you place cleanup code within a handler for the Unloaded event, such as for a Window or a UserControl, it may not be called as expected" – gusmally supports Monica Jun 10 '19 at 18:11
3

Since you're using region navigation, I would recommend using a simple region behavior that will call your interface methods whenever the view is removed from the region. I show an example of this in my Pluralsigh course: https://www.pluralsight.com/courses/prism-problems-solutions

  • Thank you for your answer. Can you just indicate in which part of your course you address this issue?Otherwise, I'm using navigation, but not only. I've also some views, which are composed by several sub views that are directly instantiated here. They have their own ViewModel, and those ViewModel are also AutoWired by the ViewModelLocator. Since they are not in a region, I guess that for them, the region behavior would not work? – J4N Jun 09 '16 at 11:52
  • Look in the Injecting Dynamic Ribbon Tabs -> Custom Region Adapter. Correct, this will not work for controls you manually creating in XAML. Though you could modify your region behavior to handle that, if you had a standard way of identifying the objects. –  Jun 09 '16 at 13:51
  • Okay thank you, i will check this. And for the controls manually created but where prism inject the ViewModels, what would be your advice? – J4N Jun 09 '16 at 16:06
  • @Liero has a pretty good suggestion. Though I would probably put those in regions too because it appears you are trying to treat them as separate views instead of just a UserControl as part of the main view which inherits the parent dataContext. –  Jun 09 '16 at 16:13
  • @J4N: If you have views managed by RegionManager that contains subviews, you can also have viewmodels that contains subviewmodels. It makes things a lot easier. Disposing the parent view will the dispose also subviews – Liero Jun 10 '16 at 17:09
  • Hi Brian, I've started to implement my regionBehavior, but currently, nothing is removing the differents view from my region, how/when should I trigger that? – J4N Jul 12 '16 at 11:11