View Cache

Coordinator
Jan 7, 2011 at 7:53 PM

In CM, all Screens implement IViewAware and cache their views by default. Currently, the only way to change this is by overriding AttachView. Should I create a property on Screen, something like ViewCacheStrategy, to make it easy to change this behavior. Values would be things like: AlwaysCache (default for backwards compat), FlushOnDeactivate and FlushOnclose. Thoughts? It seams it's pretty common for people to need to change this behavior.

Jan 7, 2011 at 8:51 PM

I've happened to customize that behavior (not very frequently, though), but I did it at IViewLocator side, where you also have knowledge of the control whose Content is being injected with the VM's view: this enable some checks on the hosting visual tree, too.
Anyway, providing a shortcut at VM side is certainly useful; plus it allows a more fine-grained customization.

Jan 8, 2011 at 2:51 AM
Edited Jan 8, 2011 at 2:57 AM

I think that the default IViewAware implementation shouldn't provide view caching at all. In my opinion, the lifecycle of a view should not depend on the implementation of an interface that is supposed to let the view-model be aware of an associated view.

I prefer to define a specialized interface or service to deal with view caching and, in my opinion, the ViewModelBinder is a good insertion point to provide view-caching (since view-model, view and context are all available).

Following, a possible implementation of a view caching solution, that follows Caliburn.Micro's 'customizable static service' approach (e.g. ViewModelBinder, ViewLocator etc.)

 namespace Caliburn.Micro
{
    #region Namespaces
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Windows;
    using Caliburn.Micro;

    #endregion

    /// <summary>
    ///   Static class used to provide view caching.
    /// </summary>
    public static class ViewCache
    {
        #region ViewCachingMode enum
        /// <summary>
        ///   Enumerator used to define the view caching mode.
        /// </summary>
        public enum ViewCachingMode
        {
            /// <summary>
            ///   Cache the view forover (i.e. until it is replaced).
            /// </summary>
            Forever = 0,
            /// <summary>
            ///   Do not cache the view.
            /// </summary>
            Never = 3,
            /// <summary>
            ///   Cache the view until view-model deactivation.
            /// </summary>
            UntilDeactivation = 1,
            /// <summary>
            ///   Cache the view until view-model close.
            /// </summary>
            UntilClose = 2
        }
        #endregion

        #region Static Fields
        /// <summary>
        ///   The current caching mode.
        /// </summary>
        private static ViewCachingMode? cachingMode;

        /// <summary>
        ///   The callback used to perform caching.
        /// </summary>
        public static Action<object, DependencyObject, object, ViewCachingMode> Cache = (viewModel, view, context, mode) =>
                                                                                        {
                                                                                            //If no caching is required, do nothing...
                                                                                            if (mode == ViewCachingMode.Never)
                                                                                                return;

                                                                                            //Get a value indicating if the cache contains a view already...
                                                                                            var hadCachedViews = GetCachedViews(viewModel).Any();

                                                                                            //Set the newly cached view...
                                                                                            SetCachedView(viewModel, view, context);

                                                                                            //If caching mode is 'forever', the view will be cached until replaced, so nothing to do...
                                                                                            if (mode == ViewCachingMode.Forever)
                                                                                                return;

                                                                                            //If this is the first time a view has been cached, Deactivated event should be attached...
                                                                                            if (!hadCachedViews)
                                                                                            {
                                                                                                //If the view-model is not IDeactivate, there is no way to flush the cache, so inform the developer...
                                                                                                var deactivable = viewModel as IDeactivate;
                                                                                                if (deactivable == null)
                                                                                                {
                                                                                                    LogManager.GetLog(typeof(ViewCache)).Warn("The view-model {0} does not support {1}, the caching is forced to {2} for the object", viewModel, typeof(IDeactivate), ViewCachingMode.Forever);
                                                                                                    return;
                                                                                                }

                                                                                                //Attach the Deactivated event so that cache is flushed when needed...
                                                                                                EventHandler<DeactivationEventArgs> deactivated = null;
                                                                                                deactivated = (sender, args) =>
                                                                                                              {
                                                                                                                  //If mode is 'until deactivation' or the view is being closed flush views...
                                                                                                                  if (Mode == ViewCachingMode.UntilDeactivation || args.WasClosed)
                                                                                                                  {
                                                                                                                      FlushCachedViews(sender);
                                                                                                                      //Once flushed, the event handler can be deactivated...
                                                                                                                      ((IDeactivate)sender).Deactivated -= deactivated;
                                                                                                                  }
                                                                                                              };

                                                                                                //Attach Deactivated event...
                                                                                                deactivable.Deactivated += deactivated;
                                                                                            }
                                                                                        };

        /// <summary>
        ///   The dictionary used to store view caches.
        /// </summary>
        private static readonly Dictionary<int, Dictionary<object, DependencyObject>> cache = new Dictionary<int, Dictionary<object, DependencyObject>>();

        /// <summary>
        ///   The default context.
        /// </summary>
        private static readonly object DefaultContext = new object();
        #endregion

        #region Static Properties
        /// <summary>
        ///   Gets the current caching mode.
        /// </summary>
        public static ViewCachingMode Mode
        {
            get { return cachingMode ?? ViewCachingMode.Forever; }
        }
        #endregion

        #region Static Members
        /// <summary>
        ///   Gets the cached view.
        /// </summary>
        /// <param name = "viewModel">The view-model to retrieve the cached view for.</param>
        /// <param name = "context">The optional context.</param>
        /// <returns>The retrieved cached view or <see langword = "null" /> if no view is cached.</returns>
        public static DependencyObject GetCachedView(object viewModel, object context = null)
        {
            DependencyObject view;
            Dictionary<object, DependencyObject> dictionary;
            if (cache.TryGetValue(GetKey(viewModel), out dictionary) && dictionary.TryGetValue(context ?? DefaultContext, out view))
                return view;
            return null;
        }

        /// <summary>
        ///   Gets the cached views associated to the specified view-model.
        /// </summary>
        /// <param name = "viewModel">The view-model.</param>
        /// <returns>The collection of cached views.</returns>
        public static IEnumerable<DependencyObject> GetCachedViews(object viewModel)
        {
            Dictionary<object, DependencyObject> dictionary;
            if (cache.TryGetValue(GetKey(viewModel), out dictionary))
            {
                foreach (var view in dictionary.Values)
                    yield return view;
            }
        }

        /// <summary>
        ///   Gets the key associated to the view-model.
        /// </summary>
        /// <param name = "viewModel">The view-model.</param>
        /// <returns>The key associated to the view-model.</returns>
        private static int GetKey(object viewModel)
        {
            return RuntimeHelpers.GetHashCode(viewModel);
        }

        /// <summary>
        ///   Sets the cached view associated to the specified view-model.
        /// </summary>
        /// <param name = "viewModel">The view-model.</param>
        /// <param name = "view">The view.</param>
        /// <param name = "context">The optional context.</param>
        public static void SetCachedView(object viewModel, DependencyObject view, object context = null)
        {
            var key = GetKey(viewModel);
            Dictionary<object, DependencyObject> dictionary;
            if (!cache.TryGetValue(key, out dictionary))
                cache[key] = dictionary = new Dictionary<object, DependencyObject>();

            dictionary[context ?? DefaultContext] = view;
        }

        /// <summary>
        ///   Flushes the cached view associated to the specified view-model.
        /// </summary>
        /// <param name = "viewModel">The view-model.</param>
        /// <param name = "context">The optional context.</param>
        public static void FlushCachedView(object viewModel, object context = null)
        {
            var key = GetKey(viewModel);
            Dictionary<object, DependencyObject> dictionary;
            if (cache.TryGetValue(key, out dictionary))
            {
                context = context ?? DefaultContext;
                dictionary.Remove(context);

                if (dictionary.Count == 0)
                    cache.Remove(key);
            }
        }

        /// <summary>
        ///   Flushes all the cached views associated to the specified view-model.
        /// </summary>
        /// <param name = "viewModel">The view-model.</param>
        public static void FlushCachedViews(object viewModel)
        {
            var key = GetKey(viewModel);
            cache.Remove(key);
        }

        /// <summary>
        ///   Initializes caching with the view model binder.
        /// </summary>
        /// <param name = "mode">The caching mode.</param>
        public static void InitializeWithViewModelBinder(ViewCachingMode mode)
        {
            if (cachingMode != null)
                throw new InvalidOperationException("Caching already initialized");

            cachingMode = mode;
            ViewModelBinder.Bind += (viewModel, view, context) => Cache(viewModel, view, context, mode);
        }
        #endregion
    }
}


Note the InitializeWithViewModelBinder method, used to append a callback to the existing ViewModelBinder.Bind delegate (of course it could be placed in a static constructor to avoid a n explicit initialization).

Using this approach, the Screen implementation could be simplified removing the views dictionary and changing following methods this way:

/// <summary>
/// Attaches a view to this instance.
/// </summary>
/// <param name="view">The view.</param>
/// <param name="context">The context in which the view appears.</param>
public virtual void AttachView(object view, object context)
{
    var loadWired = ViewCache.GetCachedView(this, context) == view;
    //No need to set the cache, since the ViewCache deals with it...

    var element = view as FrameworkElement;
    if(!loadWired && element != null)
        element.Loaded += delegate { OnViewLoaded(view); };

    if(!loadWired)
        ViewAttached(this, new ViewAttachedEventArgs{ View = view, Context = context });
}

/// <summary>
/// Gets a view previously attached to this instance.
/// </summary>
/// <param name="context">The context denoting which view to retrieve.</param>
/// <returns>The view.</returns>
public virtual object GetView(object context)
{
    //Let the specialized service try to retrieve the cached view...
    return ViewCache.GetCachedView(this, context);
}

This way IViewAware is completely decoupled from  view-caching, the behaviour can be customized and the developer can define the desired behaviour with no effort at all.

Note that I did not test this implementation, since I come-up with this idea few minutes ago... if you are interested, I can unit test it and provide an example project.

Coordinator
Jan 8, 2011 at 3:49 AM

I was actually thinking of something very similar to what you came up with for the next version. I'll probably revisit this then...maybe I'll just get you to right the code for me at that time ;)

Jan 8, 2011 at 4:22 AM

He he, i'll gladly do it, in that case! :)

Coordinator
Jan 8, 2011 at 8:40 PM

Still thinking about this...we might go ahead and do something like this. I'm pondering implementing it like a strategy, similar to the way ICloseStrategy works.

Jan 9, 2011 at 1:30 AM
Edited Jan 9, 2011 at 1:28 PM

I fear that implementing caching just as a strategy would mean scatter the caches among different view-models, making debugging a bit harder.

On the other hand, a static object and a centralized implementation can be debugged more easily.

I suppose that you are interested in proposing a fine-grained solution, where caching can be defined on a per-instance basis.

In this case (which can be quite rare, I think) I would prefer to add a proper IViewCacheAware interface to support per-object custom caching logic (maybe implemented as a strategy as you were suggesting), keeping the static service as a default.

This would mean that cached views are always stored in a static cache object (accessed through carefully unit tested static methods), but the policy used to perform the caching is customized at the view-model level.

This way you would have both flexibility and centralized control.

Jan 9, 2011 at 6:06 PM
Edited Jan 9, 2011 at 6:08 PM

Even though segregating the caching responsibility in a specific component is a better solution from a design point of view, I'm afraid that it could more easily lead to some memory leakage.
The external cache, indeed, relies on both the implementation of IDeactivate (which is optional with CM conductors) and a proper handling of screen lifecycle through a conductor.
On the other side, leaving the caching *state* in the VM guarentees that the cluster view-VM gets properly disposed by GC when no longer in use, without any complex disposal pattern.

I agree with @BladeWise, however, on keeping the caching *logic* on a centralized place; the central algorithm might deal with VM particularity checking for a strategy attribute
or querying an opportune interface (maybe IViewAware itself).

I'm not sure to agree with the caching insertion point: ViewLocator seemed natural to me because it is, well..., a locator, so it should be aware of caching. 
Putting caching logic in the location phase allows to dynamically decide whether to use the (eventually) cached view instance or not based on varying factors; for example,
the "displayLocation" parameter of LocateForXXX methods could be take into account, which helps to tell if the view will be used in different Visual subtree.
Unless I'm missing something, it seems to me that if we put caching logic into binding phase, every subsequent view location will be *forced* t follow an already taken decision. 


Thoughts?

Jan 10, 2011 at 2:38 AM
Edited Jan 10, 2011 at 2:39 AM
marcoamendola wrote:

Even though segregating the caching responsibility in a specific component is a better solution from a design point of view, I'm afraid that it could more easily lead to some memory leakage.
The external cache, indeed, relies on both the implementation of IDeactivate (which is optional with CM conductors) and a proper handling of screen lifecycle through a conductor.
On the other side, leaving the caching *state* in the VM guarentees that the cluster view-VM gets properly disposed by GC when no longer in use, without any complex disposal pattern.

 

Well, good points here... having the reference stored in the VM, means that the lifecycle of the cache lasts at most as much as the VM... on the other side, if a memory leak occurs at the VM side, the view is affected too.

The good part of having centralized caching is that there is no need to re-implement the caching code every time it is needed, which is not so frequent (to be honest), but is quite bothersome in cases where Screen is just too much, or needs to be re-implemented due to the single inheritance.

Regarding the complex disposal pattern, I agree: if we want to ensure that the cache does not contain 'dirty' data, we need to check the entire register regularly and discard stuff that is no more needed (i.e. caches related to disposed view-models). Nevertheless, since such logic would be centralized, it can be tested carefully once and for all.

I still have not a definitive answer to the issue... I like the static approach because I consider it cleaner and easier to be used, but your concern about the memory leak planted the seed of dubt... :)

Let's compare the options from the point of view of cache placement:

Static cache:

  • Pros
    • No need to re-write caching code when creating a non-standard view-model
    • Simpler implementation debug and unit testing, since the code is all in one point and the cache is static
    • Allows for custom caching logic if used in conjunction with a specialized interface or attribute
    • The (customizable) default caching logic can be applied to every view-model
  • Cons
    • Stores the cached view in a static register, and can cause the view to leak in case the lifecycle of the view-model is not handled properly, or the view does not implement IDeactivable
    • To ensure that the cache contains meaningful data, and to avoid the above issue, the cache should be checked periodically (needs weak references to the view-models)

Per-instance cache:

  • Pros
    • The lifecycle of the cached view is not longer than the view-model one (unless some application logic retains a reference to the view)
    • Allows for custom caching logic
  • Cons
    • Requires to re-write caching code for each custom view-model
    • No default caching logic, unless the default implementation is used
    • No way of knowing the complete set of cached views (this can be useful to define a complex cache purging logic, like: discarding caches unused from the longest time to ensure a maximum cache size)

 I am thinking about a mixed solution: storing the cache register in a specialized object and define an interface exposing it.

public class ViewCache
{
         public void SetCachedView(object view, object context = null)
         {
               //Implementation omitted...
         }
         public object GetCachedView(object context = null)
         {
               //Implementation omitted...
         }

         public IEnumerable GetCachedViews();
         {
               //Implementation omitted...
         }
}

public interface ICachingAware
{
         ViewCache Cache { get; }
}

With something like this, there would be no need to worry about re-implementing the cache code, the cache itself would not be static, but there could be a static register of weak references for ViewCache objects.

It would be even possible to use the per-instance caching if available, or fallback to the static register if it is not available.

marcoamendola wrote:

I'm not sure to agree with the caching insertion point: ViewLocator seemed natural to me because it is, well..., a locator, so it should be aware of caching. 
Putting caching logic in the location phase allows to dynamically decide whether to use the (eventually) cached view instance or not based on varying factors; for example,
the "displayLocation" parameter of LocateForXXX methods could be take into account, which helps to tell if the view will be used in different Visual subtree.
Unless I'm missing something, it seems to me that if we put caching logic into binding phase, every subsequent view location will be *forced* t follow an already taken decision. 

Well, initializing the cache in the ViewModelBinder.Bind means that it will be populated at the bind phase, but does not imply that the view is choosen at that time, infact the ViewLocator is still responsible to pick a proper view, given all the provided information.

I was probably not clear in the description about the insertion point: I am not referring to the point where the cache is used, but to the point where the cache is created. Currently, the view cache is populated during IViewAware.ViewAttach, the proposed implementation moves caching a step before, at the ViewModelBinder.Bind stage, so that whichever view-model can have an associated cached view.

Jan 11, 2011 at 11:37 PM

Great recap!
I was apparently not taking the cache insertion into the proper account. 
I often considered the view caching sort of a "side effect" of the view reference held by VM to support MVP-style operations (and contextless coroutine invocation, too).
That is: since a VM has a reference to its view anyway, these references can *also* be used for caching purposes.

This is the reason why I think to different caching behaviours just as "different *usage* of an already existing cache". 

Maybe we should take apart two distinct concepts: 
- the cache "retention" behavior (AlwaysCache, FlushOnDeactivate and FlushOnClose)
- the cached view actual usage 

For the first concept, I like both the "distribuited" ViewCache proposal, for the very clean separation of caching concern, and the "centralized" version with periodic scavenging of the cache.
All in all, the second one might result in a simpler implementation, with less impact on existing classes. 
I can't tell which of the two best fits the CM size requirements.

There could be a small drawback, here, with FlushOnDeactivate: when a VM is in an inactive state, it would have no corresponding view, so all MVP-style facility would be unavailable.
Not to mention if we provide a NeverCache behaviour, too...
Is it an acceptable behaviour change?

 

For the second concept, I think it could still be useful for some edge cases (mainly, when you move the VM between different "zones", or you have to check the actual "host" in which the view will be inserted).
If needed, it could be easily customized tweaking the ViewLocator.LocateForModel delegate, as usual.Anyway, the ViewLocator.LocateForModel should be aware of caching: it should have the opportunity to check the (eventually) cached view
and decide whether to provide the cached instance or a fresh one.

 

Coordinator
Jan 12, 2011 at 12:12 AM

Keep the dialog going guys. This might have to wait until the next version, I don't know yet. But, keep up the discussion. It's valuable for me.

Jan 12, 2011 at 10:54 AM
Edited Jan 12, 2011 at 12:21 PM
marcoamendola wrote:

I often considered the view caching sort of a "side effect" of the view reference held by VM to support MVP-style operations (and contextless coroutine invocation, too).
That is: since a VM has a reference to its view anyway, these references can *also* be used for caching purposes.

This is the reason why I think to different caching behaviours just as "different *usage* of an already existing cache". 

Well, I must say that I didn't think about the issue from this perspective!

To be honest, I consider the VM -> V reference almost an anti-pattern, since (in my opinion) the view-model shouldn't require it at all: if all view-models are carefully designed, they should not rely on a view reference at all, at least from a theoric point of view...

Unfortunally, this approach would create very complex view-models even when dealing with simple tasks, so IViewAware is really welcome! :)

This is why I thought about the cache as a separate functionality.

marcoamendola wrote:

Maybe we should take apart two distinct concepts: 
- the cache "retention" behavior (AlwaysCache, FlushOnDeactivate and FlushOnClose)
- the cached view actual usage 

For the first concept, I like both the "distribuited" ViewCache proposal, for the very clean separation of caching concern, and the "centralized" version with periodic scavenging of the cache.
All in all, the second one might result in a simpler implementation, with less impact on existing classes. 
I can't tell which of the two best fits the CM size requirements.

I completely agree with you about the subdivision. Divide et impera! :)

Regarding the solutions, I suppose that using the non-static ViewCache approach would not be too cumbersome in CM, since it is just a matter of updating the Screen implementation.

My biggest concern with such approach is the fact that people that rolled out their own IScreen will have no caching at all by default... this means that there is an high chance that both approaches should be used to ensure retro-compatibility... all in all, I suppose that just using the static cache is enough (even to keep the assembly size small).

What are your thoughts about the purging phase? Should it be purged synchronously or asynchrnously? The purge should be triggered every time the cache is write-accessed, read-accessed or both? I would prefer an asynchronous approach, triggered on both read and write... I already have an implementation that looks like this, I can post it here and we can discuss it if we agree that the static cache is the best approach.

marcoamendola wrote:

There could be a small drawback, here, with FlushOnDeactivate: when a VM is in an inactive state, it would have no corresponding view, so all MVP-style facility would be unavailable.
Not to mention if we provide a NeverCache behaviour, too...
Is it an acceptable behaviour change? 

For the FlushOnDeactivate mode I think that it really depends on the contextual meaning of deactivation. In any case, I suppose that as long as the default behaviour is the same as the current one, there should be no problem. What I mean is that the developer is ultimately responsible for picking a flushing method that meets his/her needs.

Regarding the NeverCache mode, I suppose that we should provide a proper fallback behaviour for Screen. I suppose that the current views dictionary could be converted in a dictionary of weak reference to views, and could be used in place of the cache if it is not used...

Uhmmm... now that I think about it, the current Screen implementation could just use the dictionary of weak references instead of addressing the cache directly... this would de-couple completely view-caching from IViewAware: even if caching is not enabled, the view-model holds a reference to the view as long as the view exists.

Honestly, I already have a modified Screen implementation that holds weak references to views, instead of strong references, since it seemed to me more logical this way. If we de-couple view caching from IViewAware, this approach would be even more meaningful.

 

marcoamendola wrote:

For the second concept, I think it could still be useful for some edge cases (mainly, when you move the VM between different "zones", or you have to check the actual "host" in which the view will be inserted).
If needed, it could be easily customized tweaking the ViewLocator.LocateForModel delegate, as usual.Anyway, the ViewLocator.LocateForModel should be aware of caching: it should have the opportunity to check the (eventually) cached view
and decide whether to provide the cached instance or a fresh one.

I agree, and this means that the ViewLocator should not use IViewAware to retrieve a cached view, but ViewCache.GetCachedView or the equivalent method.

We could provide a specific delegate to define if the cache should be used

public Func<object, DependencyObject, object, UIElement, UIElement> FilterCachedView = (viewModel, displayLocation, context, cachedView) => cachedView;

called inside ViewLocator.LocateForModel, or simply create a receipe to show how to customize the ViewLocator.

 

For the sake of clearness, a small recap of what was proposed so far (seems I like pointed lists):

  • Create a static cache and a static service (the static ViewCache from my first post in this thread) that allows from some pre-defined caching modes.
  • Create a special interface to handle custom caching logic (IViewCacheAware) (the interface should just have a method invoked by ViewCache.Cache, used to implement custom caching logic).
  • Purge the static cache on each access, so that views associated to already garbage collected view-models are de-referenced (I would prefer asynchronously, but it really depends on the target platform).
  • Change Screen implementation of the IViewAware interface, so that only weak references to views are used (to avoid to interfere with the caching logic, while trying to retain the MVP-related functionalities).
  • Change ViewLocator implementation to avoid to check for IViewAware interface, and use ViewCache.GetCachedView instead.
  • Create an extensibility point to filter out cached views or create a receipe to show how to customize the ViewLocator when dealing with cached views.
Coordinator
Jan 12, 2011 at 1:50 PM

For .NET 4 and SL4 we don't need to worry about clearing out the cache as far as memory leaks go. We can use ConditionalWeakTable. But, that's not available in WP7. So, we need some sort of weak dictionary that prunes itself occasionally.

Jun 13, 2012 at 6:26 PM

I'm just wondering whether the discussion has been finished and something implemented or if you simply forgot continuing :)

Aug 15, 2012 at 1:34 PM

Any news on that?