Search code examples
c#wpfmvvmdatatemplatexaml-resources

How to get the root element inside an applied DataTemplate when it does not have a resource key?


I wish to get the root element inside an applied DataTemplate. I tried this but it does not work for me, because for the ContentPresenter returned by MyItemsControl.ItemContainerGenerator.ContainerFromItem(vm) where vm is a ViewModel, ContentPresenter.ContentTemplate is null, although ContentPresenter.Content is the corresponding data (the same ViewModel).

I would access DataTemplates as resources like here but I cannot give the DataTemplates resource keys because I want them to be automatically applied to all the items inside the ItemsControl. So I must find a way to get the DataTemplate from an item inside the ItemsControl.

I could use if-else for determining the DataTemplate resource in function of the vm.GetType() but I would like to realize what I wish without ItemContainerGenerator and according to the MVVM pattern, if possible, and without hard-coding types.

Below is what I think is relevant in the code. I use, for example, MyAudioFileSelector from MainWindow to load some settings from the data file into the UI and I am not sure what is the MVVM way of doing this.

C# from my actual project

(I suppose currently that there is only one AudioFileSelector and one ImageFileSelector, but in future probably I will have more of them.)

internal Control GetRootControlFromContentPresenter(ContentPresenter container)
{
    // what to put here?
    return null;
}
internal AudioFileSelector MyAudioFileSelector
{
    get
    {
        foreach (SettingDataVM vm in MyItemsControl.ItemsSource)
        {
            if (vm is AudioFileSettingDataVM)
            {
                return (AudioFileSelector)GetRootControlFromContentPresenter(
                    (ContentPresenter)MyItemsControl.ItemContainerGenerator.ContainerFromItem(vm));
            }
        }
        return null;
    }
}
internal ImageFileSelector MyImageFileSelector
{
    get
    {
        foreach (SettingDataVM vm in MyItemsControl.ItemsSource)
        {
            if (vm is ImageFileSettingDataVM)
            {
                return (ImageFileSelector)GetRootControlFromContentPresenter(
                    (ContentPresenter)MyItemsControl.ItemContainerGenerator.ContainerFromItem(vm));
            }
        }
        return null;
    }
}

Test example

XAML

<Window x:Class="wpf_test_6.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:local="clr-namespace:wpf_test_6"
        mc:Ignorable="d"
        Title="MainWindow" Height="202" Width="274">
    <Window.Resources>
        <DataTemplate DataType="{x:Type local:ViewModel1}">
            <TextBlock>view model 1</TextBlock>
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:ViewModel2}">
            <TextBlock>view model 2</TextBlock>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ItemsControl x:Name="MyItemsControl" Loaded="MyItemsControl_Loaded">
        </ItemsControl>
    </Grid>
</Window>

C# code-behind

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    private void MyItemsControl_Loaded(object sender, RoutedEventArgs e)
    {
        var oc = new ObservableCollection<ViewModelBase>();
        oc.Add(new ViewModel1());
        oc.Add(new ViewModel2());
        MyItemsControl.ItemsSource = oc;
        Dispatcher.BeginInvoke(new Action(() =>
        {
            var container = (ContentPresenter)MyItemsControl.ItemContainerGenerator.ContainerFromItem(oc[0]);
            // here container.ContentTemplate is null
            Debugger.Break();
        }), System.Windows.Threading.DispatcherPriority.Loaded);
    }
}
public class ViewModelBase
{
}
public class ViewModel1 : ViewModelBase
{
}
public class ViewModel2 : ViewModelBase
{
}

Another relevant question of mine is here.

Thank you.

Update 1

  1. In my actual program I have more complex DataTemplates. The TextBlock is just an example.
  2. I need a ContentTemplate to find, for a specific container/item/index, the DataTemplate that was used. I use multiple DataTemplates applied automatically based on their DataType.

Update 2

I need DataTemplates to display in a Settings window of an application different controls in an ItemsControl, each with the DataContext set to an instance of a ViewModel subtype for each setting type, e.g. CheckBoxSettingDataVM, AudioFileSettingDataVM etc. all inheriting from SettingDataVM.

Update 3

I do not want to assign the ContentTemplate property explicitly, I want to get it so, from an item (ViewModel) I can get the container (of type ContentPresenter) and from it I can get the root element inside the implicit DataTemplate for the ViewModel, which can be AudioFileSelector, ImageFileSelector or other type. I need the ContentTemplate property to be different than null so I can store a reference to the AudioFileSelector and to the ImageFileSelector and maybe others in the future. I will use these references to load some settings from the application's opened file into these Controls.

Maybe I am doing something wrong, but I am still learning MVVM. I think that my problem would be solved if I could set the DataType of the DataTemplate and, even if it has a resource key, it would still be automatically applied inside the ItemsControls in their scope.

Update 4

I tried to understand better by making this scheme, I hope it helps (I realized it just complicated things, but it is part of my question.):

schema


Solution

  • From your code-behind, you can retrieve the root visual object of an instantiated DataTemplate by an ItemsControl for a given ViewModel object by doing the following:

    //Assuming you have access to a viewModel variable and to your MyItemsControl:
    //We retrieve the generated container
    var container = MyItemsControl.ItemContainerGenerator.ContainerFromItem(viewModel) as FrameworkElement;
    //We retrieve the closest ContentPresenter in the visual tree
    FrameworkElement firstContentPresenter = FindVisualSelfOrChildren<ContentPresenter>(container);
    //We get the first child which is the root of the DataTemplate
    FrameworkElement visualRoot = (FrameworkElement)VisualTreeHelper.GetChild(firstContentPresenter, 0); //this is what you want
    

    You need this helper function which parses the visual tree down, looking for the first child of the correct type.

    /// <summary>
    /// Parses the visual tree down looking for the first descendant (or self if correct type) of the given type.
    /// </summary>
    /// <typeparam name="T">Type of the descendant to find in the visual tree</typeparam>
    /// <param name="child">Visual element to find descendant of</param>
    /// <returns>First visual descendant of the given type or null. Can be the passed object itself if type is correct.</returns>
    public static T FindVisualSelfOrChildren<T>(DependencyObject parent) where T : DependencyObject {
        if (parent == null) {
            //we've reached the end of the tree
            return null;
        }
        if (parent is T) {
            return parent as T;
        }
        //We get the immediate children
        IEnumerable<DependencyObject> children = Enumerable.Range(0, VisualTreeHelper.GetChildrenCount(parent)).Select(i => VisualTreeHelper.GetChild(parent, i));
        //We parse them to get the first child of correct type
        foreach (var child in children) {
            T result = FindVisualSelfOrChildren<T>(child);
            if (result != null) {
                return result as T;
            }
        }
        //Nothing found
        return null;
    }