MvvmCross ViewModel caching and re-initializing

2019-02-17 17:22发布

问题:

I need to be able to intercept the framework and perform so re-initialization when a ViewModel is being reloaded from the cache. Since the ViewModel is not being recreated, I can neither use the Init(), MvxViewModel.InitFromBundle, nor MvxViewModel.ReloadFromBundle methods.

I am trying to debug a situation where clicking on the back button restores a ViewModel with inconsistent state. Some sort of MvxViewModel.OnReloading() would help.

Is there a way to do this in v3?

EDIT:

Assume I have FirstPageViewModel which exposes a command to navigate to SecondPageViewModel. Based on what I am observing, if you click on the back button of the simulator while on the SecondPageView, FirstPageViewModel is not constructed. Instead, it is retrieved, I believe, from some cache, then bound to the View. This cache is possibly an implementation of IMvxSingleViewModel cache.

Thus, the regular flow after ViewModel construction, where you call Init(), InitFromBundle() and ReloadFromBundle() does not apply in this scenario. In other words, I need a way to re-initialize a ViewModel regardless of whether it was just newly constructed, or it being resurrected from a cache. If the former, I can use an Init() method. If the latter is true, there is no way to do this within the ViewModel itself.

This is the problem:

I have an instance of ICollectionService which is passed from FirstViewModel to SecondViewModel. FirstView also contains a ListView that is bound to this CollectionService. Because CollectionService is not strongly typed I can pass it around and use the appropriate item template to render its items in the view.

Before showing SecondViewModel, FirstViewModel retrieves some remote data and populates the CollectionService. When SecondViewModel is shown, its view displays data from the CollectionService using a different item template. However, if I navigate back, since FirstViewModel is still referencing the CollectionService, FirstView will render data that was used by SecondViewModel unless FirstViewModel could be re-initialized, clearing the CollectionService in the process. Maybe the approach is wrong but this is the crux of my problem.

I do not know if the platform makes a difference, as I would expect the same behavior on Windows Phone and iOS as this re-initialization will occur in the Core module. Nonetheless these are observations on Android.

TIA.

回答1:

Thanks for updating your question to provide much more information.

Using the MvvmCross approach to cross-platform development enables you to leverage native UI platforms. This does mean that you - the developer - do need to learn a bit about these platforms - and one of the key things to understand is the "view" lifecycle, including when used in navigation stacks.

By default MvvmCross does not cache view models for any significant length of time. There are occasional short-lived caches which occur during screen transitions (eg rotation) but there are no longer term caches. Instead, when a new view is created then mvx by default creates a new viewmodel to go with it - and that pair stay together "for life" - with the end of life being determined by the view.

I'd encourage you to take some time to read about the basic lifecycle and navigation paradigms on each platform.

  • on iOS this means especially learning about UiNavigationController which maintains a stack of UiViewControllers in memory (each of which will in mvx be married to an individual viewmodel)

  • the situation is similar in WindowsPhone with the RootFrame maintaining in RAM a stack of pages. There is a complication here - tombstoning - but forget about that until after you get the basic lifecycle nailed.

  • on Android, the situation is similar, but slightly different. For basic apps you can probably assume that Android will maintain a stack of Activity pages in RAM during your app's lifetime. However, Android is actually far more complex than that - the OS can remove backstack items from RAM when it decides to reclaim memory. In these cases there's some 'tombstoning' and dehydration to sometimes worry about - but, again I'd recommend you ignore that until after you have the basics under the belt.

  • on winrt, by default the situation is actually what you understood in your description - the backstack only holds state information - the views themselves aren't cached in RAM.

The above stories hopefully give you some idea of view lifecycle in navigation stacks on each platform - and hence also give you the viewmodel lifecycle too.


Now, if you are in the situation that you want your viewmodels (or some other app level objects) to know about the view visibility state, then you'll need to intercept some view events on each platform and pass these events over to the view models.

For example, your FirstViewModel could expose OnMadeVisible() as a custom Api. In that case, you could ensure this were called from OnNavigatedTo on Windows, OnResume on Android and ViewDidAppear on iOS


Alternatively if you are looking at general mechanisms for ViewModel-ViewModel communication then I would recommend you look at something like

  • the messenger use in the CollectABull tutorial in http://mvvmcross.wordpress.com
  • Greg's viewmodel result pattern on http://gregshackles.com

Note:

Obviously, navigation stacks are not the only navigation paradigm - if your app also uses flyouts, tabs, splitviews, hamburgers, etc then you'll need to understand those view lifecycles too.

If ever you are in doubt about View lifecycles, then adding trace to their constructors and key lifecycle events is a good first step,


As a final note, if you decide the default viewmodel location and viewmodel lifecycle is not what your app needs - eg if you want to use singleton viewmodels, then this can be easily achieved - look at overriding the view model locator in your App.cs class.



回答2:

Even knowing that this question is 3 years old, I'm not sure if there's a way to do it in the current version, but I did it by myself. In this example, I'll create a static class that holds all the instances of the ViewModels in the application. It starts with null static variables and gets each value when each ViewModel is instantiated (on the constructor method).

public static class ViewStackService
{
    //Stack
    private static exmp1ViewModel exmp1 = null;
    private static exmp2ViewModel exmp2 = null;
    private static exmp3ViewModel exmp3 = null;

    public static void addStackLevel(exmp1ViewModel _parent)
    {
        exmp1 = _parent;
    }

    public static void addStackLevel(exmp2ViewModel _parent)
    {
        exmp2 = _parent;
    }

    public static void addStackLevel(exmp3ViewModel _parent)
    {
        exmp3 = _parent;
    }


    public static async void burnAll()
    {

        if (exmp3 != null)
        {
            exmp3.DoBackCommand();
            await Task.Delay(250);
            exmp3 = null;
        }
        if (exmp2 != null)
        {
            //the OnResume method can be implemented here
            exmp2.DoBackCommand();
            await Task.Delay(250);
            exmp2 = null;
        }
        if (exmp1 != null)
        {
            //the OnResume method can be implemented here
            exmp1.DoBackCommand();
            await Task.Delay(250);
            exmp1 = null;
        }
    }
}

These ViewModels used as variables receive the Instances when a constructor of each ViewModel is launched:

public class exmp1ViewModel 
    : MvxViewModel
{
    public exmp3ViewModel (){
        ViewStackService.addStackLevel (this);
    }
}

The Method burnAll() will close all the ViewModels when Called. It's problematic because as I set the time that the thread will wait manually, it can have a bug in some diferent devices, deppending on its performance. But using that class, you can do some other things, like checking whether a ViewModel was already instantiated before to instantiate a new one or use the class to implement an OnResume method to be called when a ViewModel is shown again. Just keep in mind that an instance can only be used when it's not paused,that is, you can only call methods of a ViewModel when it's being used by the app.