1

In WPF if you want to move a view from one window to another, using DataTemplates will cause the view to be destroyed and recreated in the new window, which means all view state info that is not bound gets lost.

The most obvious one is scrollbar position, however I have more complex stuff in my case.

In order to avoid this, I want to keep a cache of view and view model pairs which I can pass around without the view getting recreated.

I have a class like this:

public class ViewModelPair
{
    public IViewModel ViewModel { get; set; }
    public UserControl View { get; set; }
}

which is stored as ObservableCollection<ViewModelPair> ViewModels { get; set; } in the main window view model.

Each view/view model pair has a DataTemplate defined like this:

<DataTemplate DataType="{x:Type vm:MyViewModel}">
    <v:MyView />
</DataTemplate>

However, if I want to use a TabControl, I cannot bind the content to the view and view model properties.

I want to do something like this:

<TabControl ItemsSource="{Binding ViewModels}">
    <TabControl.ContentTemplate>
        <DataTemplate DataType="{x:Type vm:ViewModelPair}">
            <ContentPresenter DataContext="{Binding ViewModel}"
                              Content="{Binding View}" />
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

However, this gives throws binding errors:

ViewModel property not found on object of type ContentControl

and

View property not found on object of type ContentControl

If I do <ContentPresenter Content="{Binding}" /> this binding works, but I just see the name of the ViewModelPair object as the DataTemplate doesn't exist for ViewModelPair (it only exists for the child View and ViewModel types.

How could I achieve this desired behaviour where can I keep the view and view model cached together, and have this work with a TabControl/TabItem?

EDIT: Here's is a very small demo repo showing the basic app architecture and the issue with moving view models (and by extension, views) between windows.

9
  • 2
    Why would you do this at all? WPF already provides a lookup mechanism that associates view models with views, namely DataTemplate with DataType. Besides that, storing references to views in a view model is not MVVM. Commented Jul 17 at 8:55
  • Because if you want to move the view to a different window, which is functionality we have, WPF will destroy and recreate the view. Meaning anything not bound, e.g. scroll position, is lost/reset. This is a way to avoid that. I have simplified the example, in reality everything is abstracted. Commented Jul 17 at 8:58
  • The "Pair" is redundant; IMO. You pass the view with the view model already "attached" via the view's DataContext (in this case). If you were pairing "types" that's one thing; but not instances that are already related. Commented Jul 18 at 2:29
  • 1
    From what I can see your Pair collection is redundant, you only need view, DataContext of that view will be your VM. I will skip how this is not MvvM anymore. But you raise a very good point of destroying and recreating of views. It is perhaps prudent to have an attached property or even behaviour which would achieve your desired effect? This way you could possibly avoid storing your views in VM altogether. Maybe this could be abstracted further with a service and a bit of DI. Commented Jul 18 at 10:19
  • 1
    @XAMlMAX here's a small demo repo: github.com/will-scc/wpf-view-cache-demo - I haven't done any DI in this case just to keep it super simple. Commented Jul 18 at 13:04

1 Answer 1

1

I have managed to get it to move the ListView from Main Window to another window and preserve the layout. There are few steps in order to achieve this.
First I used Visual Tree helper from MSDN and I placed it in a service. Then I needed to identify ContentControl from your repo so I declared it like this in xaml:

<ContentControl Name="ContentControl" Grid.Row="1" Content="{Binding LoadedVm, Mode=OneWay}" />

As you can see I added name to the control. Then I passed it to my command in Main View Model.

<Button Command="{Binding TestCommand}" CommandParameter="{Binding ElementName=ContentControl, Path=.}" Content="Test Move to new Window" />

Inside of the VM I added DeleagteCommand and here is the method used by it:

private void Test(object obj)
    {
        if (obj is FrameworkElement ui)
        {
            var cp = VisualTreeService.FindVisualChild<ContentPresenter>(ui);
            var temp = VisualTreeHelper.GetChild(cp, 0);

            if (temp is FrameworkElement fe)
            {
                var copy = XamlReader.Parse(XamlWriter.Save(fe)) as FrameworkElement;// this could be worth exploring but for now I don't see the content being generated, I think it might have something to do with ListView and its dynamic items
                WindowService.OpenNewWindow(copy);
            }
        }
    }

Obviously there should be checks in place here to avoid NullReference exception.
And finally your Window Service:

public static void OpenNewWindow(FrameworkElement ui)
    {
        var vm = ui.DataContext as IViewModel;
        Window win = new()
        {
            Title = vm.Title,
            Content = ui,
            Height = 450,
            Width = 800
        };

        win.Show();
    }

When you start the app click on "Open Example View", then scroll down on the list of GUIDs. Then click on "Test Move to new Window". You should see new window and content will have the same scroll position. This example will get you started.
I would personally add all this to an attached property for ContentControl, so it is registered as a source of views, then I would create a behaviour, which would be aware of registered content controls and be aware of current active ContentControl. Behaviour would be attached to buttons, which then could be placed in a general style of your app. This will become more complicated if you open views like this from more than 1 window but I will leave that to you, to figure that part out.

For completeness, here is the VisualTreeService class:

using System.Windows;
using System.Windows.Media;  

public class VisualTreeService
    {
        public childItem FindVisualChild<childItem>(DependencyObject obj) where childItem : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                if (child != null && child is childItem)
                {
                    return (childItem)child;
                }
                else
                {
                    childItem childOfChild = FindVisualChild<childItem>(child);
                    if (childOfChild != null)
                        return childOfChild;
                }
            }
            return null;
        }
    }
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, I'll review and check this works
This does seem to do the trick. I got thrown off by FrameworkElement? copy = XamlReader.Parse(XamlWriter.Save(fe)) as FrameworkElement; for a bit. With that, the DataContext is null so wasn't working. I'm going to explore a more MVVM approach and ensure there's better abstractions and the like, but the premise works. Thanks for your help!
You're welcome. copy was another attempt of mine. Since the child of a ContentPresenter is removed from parent window I was looking for a way to duplicate the control rather than moving it. This way you would see the list in both windows but they would desync as soon as you would scroll either of them.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.