wpfmvvmtreeviewitemtemplate

Data in TreeView.ItemTemplate HierarchicalDataTemplate gets erased when switching the Selected TreeItem


I have the following code:

Template.XAML

<Style TargetType="{x:Type HeaderedContentControl}">
    <Setter Property="Header">
        <Setter.Value>
            <ContentControl  Foreground="Red" 
                       FontFamily="Segoe UI" 
                       Margin="0,0,0,20" 
                        Content="{Binding Tag, RelativeSource={RelativeSource AncestorType=HeaderedContentControl}}" 
                       HorizontalAlignment="Center" 
                       VerticalAlignment="Center" />
        </Setter.Value>
    </Setter>
</Style>

<DataTemplate DataType="{x:Type local:ViewModel}">
    <HeaderedContentControl  xmlns:sys="clr-namespace:System;assembly=mscorlib"
                   Tag="{Binding Header}"
                   Background="SteelBlue"
                   BorderBrush="DarkSlateBlue">
    </HeaderedContentControl>
</DataTemplate>

<DataTemplate DataType="{x:Type local:ViewModel2}">
    <HeaderedContentControl  xmlns:sys="clr-namespace:System;assembly=mscorlib"
                   Tag="{Binding Header}"
                   Background="SteelBlue"
                   BorderBrush="DarkSlateBlue">
    </HeaderedContentControl>
</DataTemplate>

Windows.XAML:

<Window.DataContext>
    <local:WindowsVM x:Name="viewModel"/>
</Window.DataContext>

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Templates.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>

<StackPanel Orientation="Horizontal">
    <TreeView SelectedItemChanged="TreeView_SelectedItemChanged" ItemsSource="{Binding AllContents}">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate>
                <Label Content="{Binding Header}" />
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
    <StackPanel Orientation="Vertical">
        <ContentControl Content="{Binding SelectedItem}" />
    </StackPanel>
</StackPanel>

Windows.XAML.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        viewModel.SelectedItem = (ViewModel)e.NewValue;
    }
}

ViewModel.cs

public class WindowsVM : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public WindowsVM()
    {
        AllContents = new ObservableCollection<ViewModel>();
        AllContents.Add(new ViewModel("Item 1"));
        AllContents.Add(new ViewModel2("Item 2")); //using ViewModel("Item 2") will show the Header as it should
        SelectedItem = AllContents.First();
    }

    private ViewModel _selectedItem;
    public ViewModel SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            _selectedItem = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
        }
    }

    private ObservableCollection<ViewModel> _allContents;
    public ObservableCollection<ViewModel> AllContents
    {
        get { return _allContents; }
        set
        {
            _allContents = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllContents)));
        }
    }
}

public  class ViewModel:INotifyPropertyChanged
{
    private string _header;

    public event PropertyChangedEventHandler PropertyChanged;

    public string Header
    {
        get { return _header; }
        set
        {
            _header = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Header)));
        }
    }

    public ViewModel(string header)
    {
        Header = header;
    }
}


public class ViewModel2 : ViewModel
{
    public ViewModel2(string header) : base(header)
    {
    }
}

If you run the application, the "Item 1" will be shown as the header of the selected item, as it should.

If you click on the second tree node, I would expect "Item 2" shown as the header of the selected item, but it is not.

Here comes the interesting part, if for "Item 2", it is of the type ViewModel instead of ViewModel2, then the "Item 2" header will be shown. How come this is happening? Is this a WPF treeview bug?

I would also welcome workaround, in the spirit of MVVM model.


Solution

  • You should bind the Header property of the HeaderedContentControl to your Header source property and then define a HeaderTemplate in the HeaderedContentControl style.

    If you implement Templates.xaml like this, your example works as expected:

    <Style TargetType="{x:Type HeaderedContentControl}">
        <Setter Property="HeaderTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Label Foreground="Red" 
                           FontFamily="Segoe UI" 
                           Margin="0,0,0,20" 
                           Content="{Binding}" 
                           HorizontalAlignment="Center" 
                           VerticalAlignment="Center" />
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
    <DataTemplate DataType="{x:Type local:ViewModel}">
        <HeaderedContentControl Header="{Binding Header}"
                       Background="SteelBlue"
                       BorderBrush="DarkSlateBlue">
        </HeaderedContentControl>
    </DataTemplate>
    
    <DataTemplate DataType="{x:Type local:ViewModel2}">
        <HeaderedContentControl Header="{Binding Header}"
                       Background="SteelBlue"
                       BorderBrush="DarkSlateBlue">
        </HeaderedContentControl>
    </DataTemplate>