Chris van de Steeg
Chris van de Steeg

Reputation: 804

ViewModels, messages and lifetime

I have modified one of @slodge's samples for a problem I have with my ViewModels lifecycle.

I've modified N26 a little: https://github.com/csteeg/NPlus1DaysOfMvvmCross/tree/viewmodeldisposesample/N-26-Fraggle

This branch uses the mvxmessenger plugin to be able to show you where things go wrong. The code isn't pretty, but shows you wat is incorrect. You can see how SubViewModel with Id = 0 keeps receiving messsages, even when it's view is long gone. And also (at some point) how HomeViewModel stops receiving messages.

Steps to reproduce (including a cleaned up version of the debug output):

start the app

HomeViewModel:Warning:HomeViewModel 0 received: Created HomeViewModel0

Click on button '1'

HomeViewModel:Warning:HomeViewModel 0 received: Created SubViewModel0 SubViewModel:Warning:SubViewModel 0 received: Created SubViewModel0 HomeViewModel:Warning:HomeViewModel 0 received: Created FirstViewModel0 SubViewModel:Warning:SubViewModel 0 received: Created FirstViewModel0 FirstViewModel:Warning:FirstViewModel 0 received: Created FirstViewModel0

Click on back

HomeViewModel:Warning:HomeViewModel 0 received: Destroyed FirstView for viewmodel 0 SubViewModel:Warning:SubViewModel 0 received: Destroyed FirstView for viewmodel 0 FirstViewModel:Warning:FirstViewModel 0 received: Destroyed FirstView for viewmodel 0 HomeViewModel:Warning:HomeViewModel 0 received: Destroyed SubFrag for viewmodel 0 SubViewModel:Warning:SubViewModel 0 received: Destroyed SubFrag for viewmodel 0 FirstViewModel:Warning:FirstViewModel 0 received: Destroyed SubFrag for viewmodel 0 HomeViewModel:Warning:HomeViewModel 0 received: Destroyed DubFrag for viewmodel 0 SubViewModel:Warning:SubViewModel 0 received: Destroyed DubFrag for viewmodel 0 FirstViewModel:Warning:FirstViewModel 0 received: Destroyed DubFrag for viewmodel 0 You can see the views getting destroyed here, I'd expect the viewmodels to go with them

Click button '1' again

HomeViewModel:Warning:HomeViewModel 0 received: Created SubViewModel1 SubViewModel:Warning:SubViewModel 0 received: Created SubViewModel1 FirstViewModel:Warning:FirstViewModel 0 received: Created SubViewModel1 SubViewModel:Warning:SubViewModel 1 received: Created SubViewModel1 HomeViewModel:Warning:HomeViewModel 0 received: Created FirstViewModel1 SubViewModel:Warning:SubViewModel 0 received: Created FirstViewModel1 FirstViewModel:Warning:FirstViewModel 0 received: Created FirstViewModel1 SubViewModel:Warning:SubViewModel 1 received: Created FirstViewModel1 FirstViewModel:Warning:FirstViewModel 1 received: Created FirstViewModel1

Here you see, subviewmodel 0 is still receiving messages. Can I somehow tell it should stop sending messages to viewmodels not attached? OR could the viewmodel know about not being attached

Now, as you continue to to repeat these steps for quite some time, say 15 times in the emulator, some viewmodels will stop receiving messages (I guess they're garbage collected). Strange thing is that one of those views is HomeViewModel! The HomeView is never destroyed, yet the homeviewmdoel stops receiving messages, thus nog being able to update the view accordingly if you app requires that

Upvotes: 2

Views: 2093

Answers (2)

Ben Gladman
Ben Gladman

Reputation: 71

I had a similar situation, where MvxMessages were still being received and acted on by viewmodels which were no longer attached to views.

My solution is to add the following to the base viewmodel:

  • a Subscribe method
  • a list of unsubscribe actions (this is added to by the Subscribe method)
  • an UnsubscribeAll method

And in the Android activity OnDestoy, I call the viewmodel's UnsubscribeAll.

(As a bonus, as the tokens are referenced in the unsubscribe actions, I don't need to keep another list of them)

BaseViewModel:

    #region Messenger

    /// <summary>
    /// Must set the Messenger object before doing any subscribing
    /// </summary>
    public IMvxMessenger Messenger { get; set; }

    private readonly object _messengerLock = new Object();

    private List<Action> _unsubscribeActions;

    /// <summary>
    /// Subscribe to a message, and store in a list so can be unsubscribed automatically later
    /// </summary>
    /// <typeparam name="TMessage"></typeparam>
    /// <param name="deliveryAction"></param>
    public void Subscribe<TMessage>(Action<TMessage> deliveryAction) where TMessage: MvxMessage
    {
        var messenger = Messenger;
        if (messenger == null) { return; }

        var token = messenger.Subscribe<TMessage>(deliveryAction);

        Action unsubscriber = delegate()
        {
            messenger.Unsubscribe<TMessage>(token);
        };

        lock (_messengerLock)
        {
            if (_unsubscribeActions == null)
            {
                _unsubscribeActions = new List<Action>();
            }
            _unsubscribeActions.Add(unsubscriber);
        }
    }

    /// <summary>
    /// Unsubscribe to all messages which have been previously subscribed to
    /// </summary>
    public void UnsubscribeAll()
    {
        if (_unsubscribeActions == null) { return; }
        lock (_messengerLock)
        {
            foreach (var a in _unsubscribeActions)
            {
                a();
            }
            _unsubscribeActions = null;
        }
    }

    #endregion

BaseActivity:

    protected override void OnDestroy()
    {
        var vm = ViewModel as ViewModel.BaseViewModel;
        if (vm != null) { vm.UnsubscribeAll(); }
        base.OnDestroy();
    }

Upvotes: 5

Stuart
Stuart

Reputation: 66882

MvvmCross v3 doesn't expose the ViewModel to any View lifecycle events like ViewDidAppear/Disappear, OnNavigatedTo/From, OnPause/OnResume/OnDestroy.

The reason for this is because:

  • earlier MvvmCross versions tried to do this and came unstuck with iOS changes in ViewDidUnload and with iOS support in general (it was hard to work out when a view was dead for good)
  • when Views are not pages - but are instead tabs, flyouts, splitviews, popups, etc - then it was hard to support and confusing to developers

Instead, MvvmCross v3 makes use of Garbage Collection to tidy up the ViewModels. To assist with this Mvx always uses WeakReferences from ViewModel-to-View and from MessageHub-to-ViewModels. The general philosophy followed is:

  • the only thing that keeps a strong reference on a View is the operating system.
  • the only thing that keeps a strong reference on a ViewModel is its View.

With that said, if GarbageCollection isn't timely enough for your app, then MvvmCross does allow you to extend your ViewModels with new functionality. For example, you could easily add a new IViewLifecycleAware interface if you want to. This is easy to do, but once done then it's your app's responsibility to ensure that interface is called from appropriate View events/overrides on each platform you support.

There's a little more on this topic in:


For your specific problem with HomeViewModel stopping receiving messages, I think that is due to you not storing the subscription token.

Because the MvvmCross messenger uses weak referencing by default, you must store the subscription token - when that token is disposed or garbage collected then the subscription will be unsubscribed.

So your code:

public class HomeViewModel
    : MvxViewModel
{
    public static int IdCounter = 0;
    public int Id = IdCounter++;
    public HomeViewModel()
    {

        var messenger = Mvx.Resolve<IMvxMessenger>();
        messenger.Subscribe<JustAMessage>(OnMessage);
        messenger.Publish(new JustAMessage(this) { Message = "Created HomeViewModel" + Id });
    }

    // ...

needs to be:

public class HomeViewModel
    : MvxViewModel
{
    public static int IdCounter = 0;
    public int Id = IdCounter++;

    private IDisposable _token;

    public HomeViewModel()
    {

        var messenger = Mvx.Resolve<IMvxMessenger>();
        _token = messenger.Subscribe<JustAMessage>(OnMessage);
        messenger.Publish(new JustAMessage(this) { Message = "Created HomeViewModel" + Id });
    }

    // ...

There's more about this in:

Upvotes: 4

Related Questions