Reputation: 804
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):
HomeViewModel:Warning:HomeViewModel 0 received: Created HomeViewModel0
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
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
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
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:
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
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:
ViewDidUnload
and with iOS support in general (it was hard to work out when a view was dead for good)Instead, MvvmCross v3 makes use of Garbage Collection to tidy up the ViewModels. To assist with this Mvx always uses WeakReference
s from ViewModel-to-View and from MessageHub-to-ViewModels. The general philosophy followed is:
With that said, if GarbageCollection isn't timely enough for your app, then MvvmCross does allow you to extend your ViewModel
s 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