Windows Store App - navigation - keep ViewModels alive

Topics: Questions
Apr 30, 2013 at 4:32 PM
Hello,

I'm trying my hands on using MVVM for a windows store app and out of the box at every navigation (using INavigationService) the viewmodel gets recreated, now I can see this being a problem in my case, is there a clever way of persisting the ViewModels or is the only way to extend the default implementation of the navigation service?

Any suggestions appreciated.

P.S. I haven't touched Catel in a while and I love the fact that it's still actively worked on and improved with every tick, well done.
Coordinator
Apr 30, 2013 at 7:39 PM
Hi,

You can create your own implementation of the IViewModelFactory and register it in the service locator. Then you are in full control of everything.
May 1, 2013 at 11:55 AM
Hi Geert,

thank you for the quick reply. What I've done is inherited Catels ViewModelFactory and overriden the 'CreateViewModel' method to catch created viewmodels and then return an already created one if exists. Registered in the ServiceLocator but now when the App starts on the constructor of 'MainPage' I get the following exception:
   at Windows.ApplicationModel.Resources.ResourceLoader..ctor(String name)
   at Catel.ResourceHelper.GetResourceManager(Assembly assembly, String resourceFile)
   at Catel.ResourceHelper.GetString(Type callingType, String resourceFile, String resourceName)
   at Catel.MVVM.InvalidViewModelException..ctor()
   at Catel.Windows.Controls.MVVMProviders.Logic.NavigationLogicBase`1.EnsureViewModel()
   at Catel.Windows.Controls.MVVMProviders.Logic.NavigationLogicBase`1..ctor(T targetPage, Type viewModelType)
   at Catel.Windows.Controls.MVVMProviders.Logic.NavigationPageLogic..ctor(Page targetPage, Type viewModelType)
   at Catel.Windows.Controls.Page..ctor()
   at ...Views.MainPage..ctor() in ...
Now I've looked through the source code and I think what happens is that LogicBase has a field:
private readonly IViewModelFactory _viewModelFactory;
Which gets a reference to the old service before I register my type? Correct me if I'm wrong please. I register my service by an instance (didn't work well with type for some reason) as early as possible I think:
public App()
        {
            var service = new Services.PersistentViewModelFactory((Catel.IoC.TypeFactory)Catel.IoC.ServiceLocator.Default.GetService(typeof(Catel.IoC.ITypeFactory)));
            Catel.IoC.ServiceLocator.Default.RegisterInstance<Catel.MVVM.IViewModelFactory>(service);
Any suggestions please or am I missing the actual problem ?

Thanks.
Coordinator
May 1, 2013 at 1:05 PM
Edited May 1, 2013 at 1:05 PM
Well, it looks like 2 problems here:

1) The view model factory seems not to be able to work
2) The ResourceManager is unable to load the right resource (but this is just a side-effect)

If you put a breakpoint in the custom factory, is it being hit?

Also another tip:

You don't have to do the custom injection in your app constructor. Just do it like this:

ServiceLocator.RegisterType<IViewModelFactory, PersistentViewModelFactory>();

As soon as it is created, the TypeFactory will inject both the ITypeFactory and IServiceLocator.
May 1, 2013 at 1:49 PM
Right... I haven't noticed that the viewmodel 'created' previously was null. On the other hand if I try to 'RegisterType' like so:
ServiceLocator.Default.RegisterType<IViewModelFactory, PersistentViewModelFactory>(RegistrationType.Singleton, true);
I can see the type registered if I drill down to the ServiceLocator while debugging but the breakpoint on the constructor never gets hit and I get this exception:
   at Catel.Windows.Controls.MVVMProviders.Logic.LogicBase.ConstructViewModelUsingArgumentOrDefaultConstructor(Object injectionObject, Type viewModelType)
   at Catel.Windows.Controls.MVVMProviders.Logic.LogicBase.ConstructViewModelUsingArgumentOrDefaultConstructor(Object injectionObject)
   at Catel.Windows.Controls.MVVMProviders.Logic.NavigationLogicBase`1.EnsureViewModel()
   at Catel.Windows.Controls.MVVMProviders.Logic.NavigationLogicBase`1..ctor(T targetPage, Type viewModelType)
   at Catel.Windows.Controls.MVVMProviders.Logic.NavigationPageLogic..ctor(Page targetPage, Type viewModelType)
   at Catel.Windows.Controls.Page..ctor()
All I've done is the following:
public class PersistentViewModelFactory : ViewModelFactory
    {
        private ConcurrentDictionary<Type, IViewModel> viewModels;

        public PersistentViewModelFactory(TypeFactory typeFactory)
            : base(typeFactory)
        {
            viewModels = new ConcurrentDictionary<Type, IViewModel>();
        }

        public override IViewModel CreateViewModel(Type viewModelType, object dataContext)
        {
            Argument.IsNotNull("viewModelType", viewModelType);

            IViewModel returnValue = null;

            if (viewModels.ContainsKey(viewModelType))
            {
                returnValue = viewModels[viewModelType];
            }
            else
            {
                var viewModel = base.CreateViewModel(viewModelType, dataContext);

                viewModels[viewModelType] = viewModel;
                viewModel.Closed += viewModel_Closed;
            }

            return returnValue;
        }

        void viewModel_Closed(object sender, ViewModelClosedEventArgs e)
        {
            e.ViewModel.Closed -= viewModel_Closed;
            var type = e.ViewModel.GetType();
            IViewModel removedViewModel = null;
            viewModels.TryRemove(type, out removedViewModel);
        }
    }
But again this never gets hit.

Thank you.
Coordinator
May 1, 2013 at 6:00 PM
Strange, I use a customized ViewModelFactory a lot and it normally works. Each LogicBase determines the IViewModelFactory as soon as it is being created. The only reason I can think of is that you are not registering the customized view model factory soon enough. Make sure that you do it before any views are loaded.
May 2, 2013 at 9:56 AM
Ok... it wasn't strange it was just the most common issue in the world - stupidity.

Besides a 'school boy' error the main issue was that in the factory in the constructor I've used 'TypeFactory' rather than 'ITypeFactory'... doh.

Oh well at least I got it sorted. Thanks for your help Geert.
Coordinator
May 2, 2013 at 10:05 AM
Glad that you sorted it out :-)
May 2, 2013 at 1:00 PM
Edited May 2, 2013 at 1:47 PM
And I'm back already! ;)

When navigating away this bit of code gets executed from the NavigationLogicBase
if (!e.Cancel && !HasHandledSaveAndCancelLogic)
            {
                if (NavigatingAwaySavesViewModel)
                {
                    SaveAndCloseViewModel();
                }
                else
                {
                    CancelAndCloseViewModel();
                }
Now I'm all for saving the ViewModel on navigation (might be some exceptions) but closing at the same time is not really great in my scenario. Unfortunately the navigation logic is a private field in the Page class so I can't really change any behaviour and my custom factory is all well and fine but every view model gets closed on the first navigation.

I really don't want to inherit/rewrite the Page and/or NavigationPageLogic class if there is something else that I could do? Ideally what would be nice is for Catel to provide a way to customise behaviour on a per view basis (should it just save, cancel and close or save and close) on navigation.

Any suggestions?
Coordinator
May 5, 2013 at 7:41 PM
We can look into this, but not on a short term. We are just finishing up Catel 3.6 and then we will take a vacation to take some rest. You are feel to create a pull request on github though ;-)