This project is read-only.

ViewModel Lifetime

May 25, 2010 at 9:43 AM
Edited May 25, 2010 at 9:57 AM

Hi Rishi

Really hope you arent getting annoyed with me asking so many questions - i am very close to finishing porting my app to nRoute and very impressed with the results.  I will post a summary of the pros/cons i have found in the process once done if you are interested.  My last hurdle is to do with the lifetime of the ViewModels.  I have noticed that when navigating (i use a combination of NavigationActions and NavigationService.Navigate() calls), the ViewModel gets recreated each time a view is visited.  I verified this in the latest SimpleMVVM sample that you posted (note in this app it does it when buttons are used to navigate, but not when DirectionalNavigateAction is used).  The problem with this, is that in my WPF app i want the state of each view to be maintained, otherwise i would have to pass the state of the application along with every navigation call.

I see that the [MapService], etc attributes allow you to define the Lifetime and was wondering if there was any such concept for MVVM maps.  I must be missing something obvious here as, although it makes sense in a web application, i cant see why views in a desktop application should be limitied to being stateless.

May 25, 2010 at 12:05 PM
Edited May 26, 2010 at 11:31 AM

Xcalibur, don’t worry the questions are all good – it makes me think too. Now, the decision to not have lifetime options for VMs is by design – and the reason is simple, each instance of a VM corresponds one-on-one to a View. And if we were to have two instances of the same Views sharing a single VM (at the same time) it could easily lead to data-integrity problems. So in a sense by having per-View VMs, I’ve tried to ensure people don’t end up shooting themselves in the foot.

 

Now, technically it is possible to share a VM with multiple Views, provided you have explicitly designed for such a scenario. I’ve indeed done so with the Officer 2010 demo, where the each of two settings panel share the same VM – but they are inheriting the VM, and I’d like to think of this as a Parent-Child(ren) scenario. There are lots of other ways to share the common state between multiple VMs, one using a shared-service, two using the built-in navigation-state capabilities, three using a Parent-Children set of VM’s where you communicate state using observable channels rather than direct references.

 

Let me know if any of the alternates works for you.

Rishi

 

May 26, 2010 at 4:27 AM

I completely agree with you reasoning - however in my program i dont want to create multiple copies of the same view, or share a VM between views.  Rather i just want to revisit and navigate between the same instances of views.  When a view is revisited, i want its viewmodel to be still in the same state it was when the view was navigated away from, rather than being reset to a new instance of the VM, which is what currently happens.  Other frameworks i have used, such as Prism, do allow this - it is up to the programmer to determine the lifetime of the VM.  Im not sure the solutions you provided really fit this situation - i could use the navigation state capabilities as you mentioned, but it seems terribly inefficient to be saving and restoring so much information when simply avoiding recreation of the VM will achieve the same result.  I hope i am explaining my situation sufficiently - if you have any other suggestions it would be much appreciated.

May 26, 2010 at 12:35 PM
Edited May 26, 2010 at 1:17 PM

Well, I can understand your point, but I still think given the state-management capabilities of nRoute I would rather manage state using the built-in mechanism than have static lifetimes for VM. For one thing, it's just too easy to leak memory when you make VMs outlive their Views – remember, normally a VM is a View's “child” that normally lives in the DataContext property of the View, which in turn means it will be GC’ed with the View, but if you turn it around you might have memory leak issues unless you are careful.

Anyhow, let me give you two possible solutions..

1. Represent the state as a DTO class.
The idea is to only presist/share the state-related data rather than having a static VM. And as shown below, you share the data using a seperate data/model class:

[MapViewModel(typeof(PageA))]
public class PageAViewModel : NavigationViewModelBase
{
    public PageAState State { get; private set; }

#region Overrides

    protected override void OnRestoreState(ParametersCollection state)
    {
        this.State = (state != null && state.ContainsKey("PAGEASTATE") ? 
            (PageAState)state["PAGEASTATE"] : new PageAState());
        base.NotifyPropertyChanged(() => State);
    }

    protected override ParametersCollection OnSaveState()
    {
        return new ParametersCollection() { new Parameter("PAGEASTATE", this.State) };
    }

#endregion

#region State Class

    [DataContract]
    public class PageAState
    {
        [DataMember]
        public string FirstName { get; set; }

        [DataMember]
        public string LastName { get; set; }
    }

#endregion

}

Note, above you might want to add the INotifyPropertyChanged related notifications to the state class.

2. Create your own IResourceLocator for static VM resolution.
Since the default behavior of MapViewModel doesn't meet your requirement you can easily extend it. Just override the MapViewModel attribute to yield your custom IResourceLocator – which would take in lifetime settings and maintain the lifetime for the IViewModelProvider resource type. It isn’t too hard:

public class MapStaticViewModelAttribute : MapViewModelAttribute
{
    public MapStaticStaticViewModelAttribute(Type viewType)
        : base(viewType) { }

    protected override nRoute.Components.Composition.IResourceLocator GetResourceLocator(Type targetType)
    {
        // return your custom resource locator which maintains the lifecycle for the VM
    }
}

With the above when you want static VMs you'll use your custom MapStaticViewModel attribute rather than built-in MapViewModel attribute.
Rishi 

May 28, 2010 at 10:03 AM
Edited Jun 16, 2010 at 4:15 AM

The first solution you presented above was the one that i was using before to get around this issue.  I have now implemented solution 2 as you suggested since it fits my needs better - for the benefit of others that may want the same behavior, here are the classes involved:

CachedViewModelLocator.cs

    using System;
    using nRoute.Components.Composition;
    using nRoute.ViewModels;

    public class CachedViewModelLocator : ResourceLocatorBase<object, ViewModelMeta>
    {
        public CachedViewModelLocator(ViewModelMeta meta) : base(meta.Name, meta)
        {
            if (meta == null)
            {
                throw new ArgumentNullException("meta");
            }
        }

        public override object GetResourceInstance()
        {
            return new CachedViewModelProvider(this.ResourceMeta);
        }
    } 

CachedViewModelProvider.cs 

    using System;
    using System.Collections.Generic;
    using nRoute.Components.Composition;
    using nRoute.ViewModels;

    public class CachedViewModelProvider : IViewModelProvider
    {
        private static IDictionary<ViewModelMeta, object> views;
        private static IDictionary<ViewModelMeta, object> viewModels;

        private readonly ViewModelMeta meta;

        static CachedViewModelProvider()
        {
            CachedViewModelProvider.views = new Dictionary<ViewModelMeta, object>();
            CachedViewModelProvider.viewModels = new Dictionary<ViewModelMeta, object>();
        }

        public CachedViewModelProvider(ViewModelMeta meta)
        {
            if (meta == null)
            {
                throw new ArgumentNullException("meta");
            }

            this.meta = meta;
        }

        public object CreateViewInstance()
        {
            if (!CachedViewModelProvider.views.ContainsKey(this.meta))
            {
                CachedViewModelProvider.views.Add(this.meta, TypeBuilder.BuildType(this.meta.ViewType));
            }

            return CachedViewModelProvider.views[this.meta];
        }

        public object CreateViewModelInstance()
        {
            if (!CachedViewModelProvider.viewModels.ContainsKey(this.meta))
            {
                CachedViewModelProvider.viewModels.Add(this.meta, TypeBuilder.BuildType(this.meta.ViewModelType));
            }

            return CachedViewModelProvider.viewModels[this.meta];
        }

        public ViewModelMeta ViewModelMeta
        {
            get
            {
                return this.meta;
            }
        }
    }

MapCachedViewModelAttribute.cs 

    using System;
    using nRoute.Components.Composition;
    using nRoute.ViewModels;

    [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    public class MapCachedViewModelAttribute : MapViewModelAttribute
    {
        private InstanceLifetime lifetime;

        public MapCachedViewModelAttribute(Type viewType) : this(viewType, InstanceLifetime.Singleton)
        {
        }

        public MapCachedViewModelAttribute(Type viewType, InstanceLifetime lifetime) : base(viewType)
        {
            this.lifetime = lifetime;
        }

        protected override IResourceLocator GetResourceLocator(Type targetType)
        {
            // Return a resource locator which maintains the lifecycle for the VM
            if (this.lifetime == InstanceLifetime.Singleton)
            {
                return new CachedViewModelLocator(new ViewModelMeta(this.ViewType, targetType));
            }
            else
            {
                return new DefaultViewModelLocator(new ViewModelMeta(this.ViewType, targetType));
            }
        }
    }

You then use the new attribute on your view model like this:

[MapCachedViewModel(typeof(TestView))]
public class TestViewModel
{
...
}

The lifetime can be specified to PerInstance, so that it then behaves the same way as before, or you can just use the standard MapViewModel attribute.  The CachedViewModelProvider also supports caching the view, but this would require another attribute to be written, which is not provided.

 

May 28, 2010 at 11:04 AM

Excellent Stuff, I'll just add that you should also create a DefineCachedViewModel counterpart for consistency sake. However, I would be warry about caching the View, because for one thing a View can't have two parents or put another way you can't insert the same View twice in the Visual Tree. Also, even if you have removed the View from a Panel, there can be times (like when using Virtualization/Container Recycling) when the View is still present in the Visual Tree for an unspecified period of time.

Anyway, thanks for sharing..
Rishi

May 28, 2010 at 11:18 AM

One more thing, maybe you can move the caching part to the your CachedViewModelLocator - in things like Services, ViewServices, etc I've put the caching logic in their IResourceLocator implementation. As you know, IResourceLocator is responsible for providing resource instances when requested, so it can cache the result within itself.

Rishi

May 31, 2010 at 6:03 AM

This was my first thought, but ultimately did not work because it merely cached the provider.  Calls to the cached provider's CreateViewModel still returned new instances, so i needed to cache it here instead.  Perhaps im missing something?

cheers

Jun 1, 2010 at 10:58 AM

Well, in your IResourceLocator you could provide an implementation of ViewModelProvider that could just lazy load and return the same instance of VM everytime. And the ViewModelProvider would be cached within the IResourceLocator.

Anyway, this is not a big deal - as the outcome is the same.
Rishi