LocateForModel and cached views

Feb 13, 2011 at 6:35 PM
Edited Feb 13, 2011 at 6:47 PM

As far as I understood (and tested) CM is using the cached view ONLY when the view isn't loaded and there is a Handle!!!

I would expect the cached view to be used when it is loaded and there is a handle.

This is the relevant excerpt from LocateForModel method

if (windowCheck == null || (!windowCheck.IsLoaded && !(new WindowInteropHelper(windowCheck).Handle == IntPtr.Zero)))
{
    Log.Info("Using cached view for {0}.", model);
    return view;
}

If I don't close the window that is cached, the window is loaded and the first condition fails. When I close the window that is cached, the first condition is fulfilled as the window isn't loaded. Of course the window has no handle so the second condition fails.

What am I doing wrong or what am I misunderstanding?

Coordinator
Feb 13, 2011 at 7:37 PM

What is it exactly you are trying to do? and what is it exactly preventing you from it?

Feb 13, 2011 at 8:21 PM

I have this DocListViewModel bound to a DocListView with a DataGrid that list documents. When I double click a row, I want to open a window showing that document but only one window per document.

So it all starts like this:

var wm = new WindowManager(); var vm = new DocEditViewModel(Parent, docID); wm.ShowWindow(vm, "WindowView");

et voila, my document shows on a Window using DocEdit.WindowView.xaml (DocEditView.xaml was temporarily needed for other purpose).

The DocEditViewModel implememts IViewAware and keeps a static Dictionary<int,object> where I keep the document's ID and the attached view like this

public void AttachView(object view, object context)
{
    if (!_views.ContainsKey(Model.DocID))
    {
        _views.Add(Model.DocID, view);
    }
}

On the other hand, when CM asks for a view, if I have one, I give it to CM like this:

public object GetView(object context)
{
    object view = null;
    if (_views.ContainsKey(Model.DocID))
        if (_views.TryGetValue(Model.DocID, out view)) // this "if" is useless as a view is returned no matter what
            return view;

    return view;
}

I must add I can't inherit from Screen as I am already inheriting from Csla.Xaml.ViewModel<Doc> (this is why I have Model.DocID instead of plain DocID). So I just implement IViewAware, IChild and IHaveDisplayName.

I didn't get to the part where I must clean the cached view but I'm planning to.

Feb 19, 2011 at 9:52 PM

What exactly is the view caching good for?

I have the opposite problem, i get a cached view when i don't want it :).

In my application i had the case that a view was already bound to a instantiated view model, but not loaded yet (and it was a user control, not a window).

Then LocateForModel was called again (via WindowMangers ShowWindow) with the same view model, so:

if (windowCheck == null || (!windowCheck.IsLoaded && !(new WindowInteropHelper(windowCheck).Handle == IntPtr.Zero)))

evaluated to true and the already existing view associated to this view model was re-used in WindowManagers EnsureWindow.

Which lead to the beloved WPF exception "Specified element is already the logical child of another element. Disconnect it first" which happens when you insert the same visual tree into two different places (i.e. windows here).

I solved my problem by shortcutting the caching with:

            ViewLocator.LocateForModel =
                (model, displayLocation, context) => { return ViewLocator.LocateForModelType(model.GetType(), displayLocation, context); };

Does Caliburn.Micro support several views over the same view model (no context involved)? If so, how can you avoid the WindowManager putting the same view object into different windows?

 

Regards,

Gunter

Mar 30, 2011 at 7:35 AM

Hi Rob,

could you clarify the logic behind view caching? Why existing window is not reused when searching for a cached view?

To illustrate my question:

    windowManager.ShowWindow(myViewModel);  // New window is created here

    windowManager.ShowWindow(myViewModel);  // I want existing window to be activated instead of creating a new window

Thanks!

Regards,

Konstantin Spirin.

Mar 30, 2011 at 9:29 AM
Edited Mar 30, 2011 at 10:07 AM

The current view caching implementation relies on the fact that the view-model implements IViewAware.

The bit that does the magic is

 

var view = EnsureWindow(rootModel, ViewLocator.LocateForModel(rootModel, null, context), isDialog);

 

since the default LocateForModel implementation will try to retrieve the view from the cache, if IViewAware is implemented:

 

        /// <summary>
        /// Locates the view for the specified model instance.
        /// </summary>
        /// <returns>The view.</returns>
        /// <remarks>Pass the model instance, display location (or null) and the context (or null) as parameters and receive a view instance.</remarks>
        public static Func<object, DependencyObject, object, UIElement> LocateForModel = (model, displayLocation, context) =>{
            var viewAware = model as IViewAware;
            if(viewAware != null)
            {
                var view = viewAware.GetView(context) as UIElement;
                if(view != null)
                {
#if !SILVERLIGHT
                    var windowCheck = view as Window;
                    if (windowCheck == null || (!windowCheck.IsLoaded && !(new WindowInteropHelper(windowCheck).Handle == IntPtr.Zero)))
                    {
                        Log.Info("Using cached view for {0}.", model);
                        return view;
                    }
#else
                    Log.Info("Using cached view for {0}.", model);
                    return view;
#endif
                }
            }

            return LocateForModelType(model.GetType(), displayLocation, context);
        };

 

This means that as long as your view-model implements IViewAware, the view should be re-used.

Apr 1, 2011 at 5:30 AM

My ViewModel is inherited from Presenter which is IViewAware but I still see two windows bound to my single ViewMode.

I've checked and the same behavior exists in Caliburn 2.0.

 

Code that you've posted only reuses window if it is not created (!IsLoaded && Handle == Zero). For Silverlight view is always reused.

I am wondering why shown Window is not reused in non-Silverlight scenario.

Apr 1, 2011 at 8:47 AM

Did you re-implement IViewAware by yourself? Or your Presenter inherits from Screen?

Would it be possible to provide a sample of the issue you are encountering?

Coordinator
Apr 1, 2011 at 2:13 PM

Yeah. I'm going to need a sample of this. Please make it as simple as possible and send it to robertheisenberg at hotmail dot com  I'm trying to get the RTW out in the next couple of weeks, so if you hesitate to send the sample, and there is a bug, that's gong to make me very sad ;(

Apr 1, 2011 at 5:24 PM

Rob, I've dropped you email

Coordinator
Apr 3, 2011 at 12:39 AM

Unfortunately, I cannot change this behavior. The reason is that a window may be cached which was previously closed. In this case the WPF runtime will not allow it to be shown again. The platform will throw an exception. To my knowledge, there is no way to tell if a window has been previously closed or not. As a result, we have to result to the following:

                    var windowCheck = view as Window;
                    if (windowCheck == null || (!windowCheck.IsLoaded && !(new WindowInteropHelper(windowCheck).Handle == IntPtr.Zero)))
                    {
                        Log.Info("Cached view returned for {0}.", model);
                        return view;
                    }

This more or less determines whether or not the Window has been shown. If it has previously been shown, we cannot reuse it without the possibility of exceptions occurring. However, it's pretty easy to change this behavior for your own needs. Simply derive from DefaultViewLocator and override LocateForModel. Your code would look something like this:
        public override DependencyObject LocateForModel(object model, DependencyObject displayLocation, object context)
        {
            if (model == null)
                return null;

            var viewAware = model as IViewAware;
            if (viewAware != null)
            {
                var view = viewAware.GetView(context) as DependencyObject;
                if (view != null)
                    return view;
            }

            var createdView = LocateForModelType(model.GetModelType(), displayLocation, context);
            InitializeComponent(createdView);
            return createdView;
        }
Alternatively, you could work out a special interface the locator or window manager recognized in order to just re-activate windows. Another idea would be to create a custom attached property on your window that allows it to be activated/deactivate along with the underlying view model (I should probably do that in a future release).
Finally, if you are starting a new project, I highly encourage you to investigate Caliburn.Micro. You will find it's quite easy to customize for this scenario as well.