Search code examples
c#wpfxamldata-bindingdatatemplate

How to dynamically change DataTemplate according to bound object's type?


I'm trying to create a DataTemplate for a View, to show a specific UserControl type (like a texbox, combobox, custom control or another View) based on the type of object it is bound to.

I have the following MVVM framework:

FieldView is tied to an instance of FieldPresenter, and should display a <Textblock /> for the "Label" property, and a UserControl or another View for the Value (based on the Type of the value), with it's DataSource set to the Value property of the Presenter. Currently, I do not have the second part working. I can't figure out how to write a WPF template for what I need.

ViewModel:

public class FieldPresenter : Observable<object>, IFieldPresenter, INotifyPropertyChanged
{
    public FieldPresenter() { }
    public FieldPresenter(object value)
    {
        Value = value;
    }
    object IFieldPresenter.Value
    {
        get
        {
            return base.Value;
        }

        set
        {
            base.Value = value;
            OnPropertyChanged("Value");
        }
    }
    private string _label;
    public virtual string Label
    {
        get
        {
            return _label;
        }
        private set
        {
            _label = value;
            OnPropertyChanged("Label");
        }
    }
}

View:

<UserControl x:Class="My.Views.FieldView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:ViewModels="clr-namespace:My.ViewModels"
             mc:Ignorable="d" 
             d:DesignHeight="24" d:DesignWidth="100">
    <UserControl.DataContext>
        <ViewModels:FieldPresenter/>
    </UserControl.DataContext>
        <UserControl.Template>
            <ControlTemplate>
                <Grid Margin="4">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" SharedSizeGroup="Key" />
                    </Grid.ColumnDefinitions>
                    <StackPanel Margin="0,0,0,0" HorizontalAlignment="Stretch" Width="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=ActualWidth}">
                        <TextBlock Text="{Binding Label}" FontWeight="Bold" Height="32" HorizontalAlignment="Stretch"/>
                        <TextBox Text="{Binding Value}" Height="Auto" HorizontalAlignment="Stretch"/>
                    </StackPanel>
                </Grid>
            </ControlTemplate>
        </UserControl.Template>
</UserControl>

I'm curious if what I'm trying to do is even possible, or if I can workaround it by making my Presenter viewmodel return a UserControl rather than an object value, and have the Presenter parse the UserControl Type from the object type, but I don't feel like my Presenter should be instantiating Controls (or what is technically an unbound view). Should I make an interface, something like IViewAs<controlType> { controlType View { get; } }?

How else would I replace <TextBox Text="{Binding Value}" /> in the above script with some kind of template of a UserControl based on the databound object's type?


Solution

  • You almost certainly want a ContentTemplateSelector :

    Code:

    using System.Windows;
    using System.Windows.Controls;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                Primitive primitive;
    
                primitive = new Sphere();
                //  primitive = new Cube();
                DataContext = primitive;
            }
        }
    
        internal abstract class Primitive
        {
            public abstract string Description { get; }
        }
    
        internal class Cube : Primitive
        {
            public override string Description
            {
                get { return "Cube"; }
            }
        }
    
        internal class Sphere : Primitive
        {
            public override string Description
            {
                get { return "Sphere"; }
            }
        }
    
        public class MyTemplateSelector : DataTemplateSelector
        {
            public override DataTemplate SelectTemplate(object item, DependencyObject container)
            {
                var frameworkElement = container as FrameworkElement;
                if (frameworkElement != null && item != null)
                {
                    if (item is Cube)
                    {
                        return frameworkElement.FindResource("CubeTemplate") as DataTemplate;
                    }
                    if (item is Sphere)
                    {
                        return frameworkElement.FindResource("SphereTemplate") as DataTemplate;
                    }
                }
    
                return base.SelectTemplate(item, container);
            }
        }
    }
    

    XAML:

    <Window x:Class="WpfApplication1.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:local="clr-namespace:WpfApplication1"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            x:Name="Window"
            Title="MainWindow"
            Width="525"
            Height="350"
            mc:Ignorable="d">
    
        <Grid>
            <Grid.Resources>
                <local:MyTemplateSelector x:Key="myTemplateSelector" />
                <DataTemplate x:Key="CubeTemplate" DataType="local:Cube">
                    <Border BorderBrush="Blue"
                            BorderThickness="1"
                            CornerRadius="5" />
                </DataTemplate>
                <DataTemplate x:Key="SphereTemplate" DataType="local:Sphere">
                    <Border BorderBrush="Red"
                            BorderThickness="1"
                            CornerRadius="50" />
                </DataTemplate>
            </Grid.Resources>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="1*" />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Label Grid.Row="0"
                   Content="{Binding Description}"
                   d:DataContext="{d:DesignInstance local:Primitive}" />
            <ContentControl Grid.Row="1"
                            Content="{Binding}"
                            ContentTemplateSelector="{StaticResource myTemplateSelector}" />
    
        </Grid>
    </Window>
    

    Result:

    enter image description here

    enter image description here

    See the documentation for more:

    https://msdn.microsoft.com/en-us/library/system.windows.controls.datatemplateselector(v=vs.110).aspx