1
votes

I have a content control as follows:

<ContentControl x:Name="grid1ContentControl" Content="{Binding MainGridViewModel}" />

MainGridViewModel is a property of type MainGridViewModelType.

I also have a DataTemplate as follows:

<DataTemplate DataType="{x:Type App:MainGridViewModelType}">...

I initialize (ie, set) the MainGridViewModel property and raise the NotifyPropertyChanged event.

It is my expectation that at this moment, the framework should be notified that I set MainGridViewModel and, as a result of the Binding on ContentControl, add the DataTemplate contents matching the MainGridViewModel property type (ie, MainGridViewModelType) to the visual tree at the location where the ContentControl is located.

Indeed, I can see my RaisePropertyChanged() method run on the MainGridViewModel property's setter. However, when inspecting the visual tree using the visual tree inspector, the ContentControl's ContentPresenter does not show the contents of my DataTemplate following the initialization of MainGridViewModel. Why?

Note, after setting MainGridViewModel again in response to a user interaction, I get the update to the visual tree that I expected.

The only workaround I have come up with is to give the DataTemplate a x:Key, and set the ContentControl's Content value explicitly, and not rely on the framework matching the type to its DataTemplate, and applying it for me:

ContentControl grid1ContentControl = VisualElementFinder.FindDescendantByName(mwin, "grid1ContentControl") as ContentControl;
grid1ContentControl.SetValue(ContentControl.ContentTemplateProperty, mwin.FindResource("MainGridViewModelKey") as DataTemplate);

I keep running into the same problem. There is a gap in my understanding of how the framework is expected to react to the subsequent assignment of a bound property after the visual has been initialized. I have taken Will's suggestion and developed a prototype, as described below. I appreciate any further thoughts.

Here is a link to my blog entry on the subject. Here is a direct link to the prototype project.

Here is the MainWindow code to provide context for further discussion. In the prototype project, NewTabControlViaContentControlCommand works, while NewTabControlCommand does not.

MainWindow.xaml:

    <Window x:Class="DynamicTabControlSimpleProto.View.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:DynamicTabControlSimpleProto.ViewModel"
        xmlns:vw="clr-namespace:DynamicTabControlSimpleProto.View"
        xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
        mc:Ignorable="d"
        Height="300"
        Width="500"
        Title="Dynamic TabControl Prototype"
        DataContext="{Binding Main, Source={StaticResource Locator}}">

    <Window.Resources>
        <DataTemplate DataType="{x:Type vm:TabControlViewModel}">
            <vw:TabControlUserControl />
        </DataTemplate>

        <DataTemplate x:Key="tabControlDataTemplate">
            <vw:TabControlUserControl />
        </DataTemplate>

    </Window.Resources>

    <Grid x:Name="LayoutRoot">

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="4" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <StackPanel Name="stackPanel" Grid.Column="0">
            <Button
                Content="NewTabControl" 
                Command="{Binding NewTabControlCommand}"
                CommandParameter="{Binding Path=DataContext}" />

            <Button
                Content="NewTabControlViaContentControl" 
                Command="{Binding NewTabControlViaContentControlCommand}"
                CommandParameter="{Binding ElementName=tabControlContentControl}" />
        </StackPanel>

        <DockPanel
            Name="dockPanel"
            Grid.Column="2">
            <Border 
                BorderBrush="Blue"
                BorderThickness="5">
                <ContentControl x:Name="tabControlContentControl" DataContext="{Binding TabControlViewModel, diag:PresentationTraceSources.TraceLevel=High}" />
            </Border>
        </DockPanel>
    </Grid>
</Window>

MainWindowViewModel.cs

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Windows.Controls;
using DynamicTabControlSimpleProto.View;
using System.Windows;

namespace DynamicTabControlSimpleProto.ViewModel
{

    public class MainViewModel : ViewModelBase
    {
        public string Welcome
        {
            get
            {
                return "Welcome to MVVM Light";
            }
        }

        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        public MainViewModel()
        {
            NewTabControlCommand = new RelayCommand<object>(obj =>
                {
                    this.NewTabControl(obj);
                });

            NewTabControlViaContentControlCommand = 
                new RelayCommand<ContentControl>(tabControlContentControl =>
                {
                    this.NewTabControlViaContentControl(tabControlContentControl);
                });


        }

        public TabControlViewModel TabControlViewModel
        {
            get
            {
                return _tabControlViewModel;
            }
            private set
            {
                _tabControlViewModel = value;
                RaisePropertyChanged("TabControlViewModel");
            }
        }

        public RelayCommand<object> NewTabControlCommand
        {
            get;
            private set;
        }

        private TabControlViewModel _tabControlViewModel = null;

        void NewTabControl(object obj)
        {
            TabControlViewModel = new TabControlViewModel();
        }

        public RelayCommand<ContentControl> NewTabControlViaContentControlCommand
        {
            get;
            set;
        }

        void NewTabControlViaContentControl(ContentControl tabContentControl)
        {
            TabControlViewModel = new TabControlViewModel();
            MainWindow mwin = Application.Current.MainWindow as MainWindow;
            tabContentControl.SetValue(ContentControl.ContentTemplateProperty, mwin.FindResource("tabControlDataTemplate") as DataTemplate);
        }



    }
}
1
Why? Could be one of hundreds of reasons. Is MainGridViewModel a property off of the instance that is set on the DataContext of the ContentControl? Are you doing all of this before InitializeComponent has been called in the constructor? Etc etc on and on. You need to create the simplest possible, compilable prototype that does what you want and see if you can repro your issue. Then use that here (code in question, no links, please) to illustrate your problem. But most likely you'll solve your issue yourself.user1228
@Will: Yes, MainGridViewModel is a property of the instance that is the DataContext of the ContentControl. Setting MainGridViewModel occurs after InitializeComponent has been called. The framework doesn't "apply the datatemplate" the first time; subsequent setting of MainGridViewModel has the intended effect. I will try to create a simple reproduction; it just seems like I need to re-trigger the binding, or, perhaps add a new binding to get ContentControl to add the type-linked DataTemplate to its ContentPresenter, just as setting ContentControl.ContentTemplateProperty directly does.Bill
What you said leads me to believe that it should work. Have done it many many times. I think your proto will clarify your issue.user1228
@Will: Hi Will, I took your suggestion and put together a prototype. It is really simple and illustrates both the technique that is not working as I would expect, as well as the one that does work. I included links to a VS2010 project that compiles, and added code and a brief description in my original post in order to provide context for discussion. I appreciate any additional insights you could provide. Thanks.Bill
Cool. I'll take a look at it tomorrow.user1228

1 Answers

1
votes

I believe that I found the error in the technique that was not working above. I found it by trying to set the DataContext directly in XAML the same way I was doing in code - namely, directly to a ViewModel. When I tried this, it too didn't work.

For example, setting the DataContext of my MainWindow to a ViewModel does not work:

<Window DataContext="vm:MainViewModel" ...>

However, setting the DataContext of my Window to a ViewModel declared in the resources of a higher level component - in my case, App.xaml, allows everything to work fine:

In App.xaml:

<Application.Resources>
    <vm:MainViewModel x:Key="MainViewModel" />
</Application.Resources>

Then, in MainWindow.xaml:

<Window DataContext="{Binding Source={StaticResource MainViewModel}}" ...>

I think what I was doing in my original code in the example above is similar to setting the the DataContext directly to the ViewModel, because, even though my ContentControl was creating a Binding, I think the necessary element must be that the ViewModel, one way or another, needs to be added as a Resource. Only then can the DataContext Binding on the ContentControl be set to the resource and the framework will then provide the expected mapping of the ViewModel type to the DataTemplate visual tree elements.

All of the above information is useful for ContentControls. In particular, the code solution in my original question may be the best and only way to achieve what is described. However, the story is different for an ItemsControl. At the risk of this post diverging too far, I included these additional insights in a post on my blog.