1

This question is related to the accepted answer in my question: Dynamic content in DataGrid or GridView CellTemplate

I have a collection of different datatypes to the ItemSource of the DataGrid, and depending on the datatype of the current index in the DataSignalModel collection, the corresponding DataType is to be used.

This the solution that helped me in the linked question:

As you store a data type member instead of creating two distinct model types, you will have to create a custom DataTemplateSelector for selecting the data template depending on DataType.

public class DataSignalModelTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (!(item is SignalDisplayModel dataSignalModel) || !(container is FrameworkElement containerFrameworkElement))
            return null;

        switch (dataSignalModel.DataType)
        {
            case DataType.Bool:
                return FindDataTemplate(containerFrameworkElement, "DataSignalModelBoolTemplate");
            case DataType.Int:
            case DataType.Float:
                return FindDataTemplate(containerFrameworkElement, "DataSignalModelNumericTemplate");
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

    private static DataTemplate FindDataTemplate(FrameworkElement frameworkElement, string key)
    {
        return (DataTemplate)frameworkElement.FindResource(key);
    }
}

And I want to follow the advice below.

A note on data types: You keep a member DataType in your DataSignalModel to determine its type. Usually you would create specialized types for each data and not a one-for-all mode

So when I change my design to have an interface or base class and concrete implementation of the different types, how do I adapt my current solution to use the different DataTemplate's?

  • Get rid of the custom DataTemplateSelector and somehow bind the different DataTemplates to the corresponding IDataSignalModel implementations?
  • Adapt the custom DataTemplateSelector to return the DataTemplate on item type?
  • Or maybe a different approach?
NorwE
  • 79
  • 1
  • 4
  • Not sure why you think you would even need a common interface or a DataTemplateSelector. You could put instances or arbitrary classes into an `ObservableCollection` used as ItemsSource. Then have DataTemplates with appropriate DataTypes for all types you need. – Clemens Feb 13 '21 at 17:43
  • Hey, did you find a solution to this? – Mike Nakis Jun 11 '23 at 14:01
  • Does this answer your question? [How to bind DataTemplate datatype to interface?](https://stackoverflow.com/questions/15023441/how-to-bind-datatemplate-datatype-to-interface) – Mike Nakis Jun 11 '23 at 17:01

2 Answers2

0

As said in the previous answer, you would create distinct types for your data, e.g.:

public class BoolDataSignalModel: INotifyPropertyChanged
{
   // ...
}
public class IntDataSignalModel: INotifyPropertyChanged
{
   // ...
}

Just create as many models as you need. They do not necessarily have to share a common base type, it does not matter for data templating. I guess your data collection is still exposed like this:

public IList<DataSignalModel> DataSignalsList
{
   get { return _dataSignalsList; }
   set { _dataSignalsList = value; }
}

If you do not change your collection at runtime this is alright, otherwise use ObservableCollection<T>. Now, define a data template for each model in XAML with the DataType set to your model type, e.g.:

<DataTemplate DataType="{x:Type local:BoolDataSignalModel}">
   <StackPanel Orientation="Horizontal" Margin="5">
      <Button Content="Click Me" FontSize="10" Height="18"/>
   </StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type local:IntDataSignalModel}">
   <StackPanel Orientation="Horizontal">
      <TextBlock Text="0" Margin="10 5 10 5"/>
      <Slider Value="50" Minimum="0" Maximum="100" MinWidth="150"/>
      <TextBlock Text="100" Margin="10 5 10 5"/>
   </StackPanel>
</DataTemplate>

Please note, that you do not define an x:Key, otherwise it will not work. If you put these data templates in a resource dictionary in scope of the items control that you bound your collection to, it will automatically select the appropriate data template based on the item type. No need for a data template selector or a base class.

For more information, refer to Data Templating Overview.

thatguy
  • 21,059
  • 6
  • 30
  • 40
0

Here is my InheritanceDataTemplateSelector which just works with interfaces:

namespace MyWpf;

using Sys = System;
using Wpf = System.Windows;
using WpfControls = System.Windows.Controls;

//PEARL: DataTemplate in WPF does not work with interfaces!
//       The declaration <DataTemplate DataType="{x:Type SomeInterface}"> silently fails.
//       We solve this problem by introducing a DataTemplateSelector 
//       that takes interfaces into consideration.
//Original inspiration from https://stackoverflow.com/q/41714918/773113
public class InterfaceDataTemplateSelector : WpfControls.DataTemplateSelector
{
    delegate object? ResourceFinder( object key );

    public override Wpf.DataTemplate? SelectTemplate( object item, Wpf.DependencyObject container )
    {
        ResourceFinder resourceFinder = getResourceFinder( container );
        return tryGetDataTemplateRecursively( item.GetType(), resourceFinder );
    }

    static ResourceFinder getResourceFinder( Wpf.DependencyObject container ) //
        => (container is Wpf.FrameworkElement containerAsFrameworkElement) //
                ? containerAsFrameworkElement.TryFindResource //
                : Wpf.Application.Current.TryFindResource;

    static Wpf.DataTemplate? tryGetDataTemplateRecursively( Sys.Type type, ResourceFinder resourceFinder )
    {
        return tryGetDataTemplateFromType( type, resourceFinder ) //
                ?? tryGetDataTemplateFromInterfacesRecursively( type, resourceFinder ) //
                ?? tryGetDataTemplateFromSuperTypeRecursively( type, resourceFinder );
    }

    static Wpf.DataTemplate? tryGetDataTemplateFromType( Sys.Type type, ResourceFinder tryFindResource )
    {
        Wpf.DataTemplateKey resourceKey = new Wpf.DataTemplateKey( type );
        object? resource = tryFindResource( resourceKey );
        if( resource is Wpf.DataTemplate dataTemplate )
        {
            if( !dataTemplate.IsSealed )
                dataTemplate.DataType = type;
            return dataTemplate;
        }
        return null;
    }

    static Wpf.DataTemplate? tryGetDataTemplateFromInterfacesRecursively( Sys.Type type, ResourceFinder resourceFinder )
    {
        foreach( var interfaceType in type.GetInterfaces() )
        {
            Wpf.DataTemplate? dataTemplate = tryGetDataTemplateRecursively( interfaceType, resourceFinder );
            if( dataTemplate != null )
                return dataTemplate;
        }
        return null;
    }

    static Wpf.DataTemplate? tryGetDataTemplateFromSuperTypeRecursively( Sys.Type type, ResourceFinder resourceFinder )
    {
        return type.BaseType == null ? null : tryGetDataTemplateRecursively( type.BaseType, resourceFinder );
    }
}

How to use:

In your Resources section, define each DataTemplate as usual, where now each DataType is an interface instead of a concrete type:

<DataTemplate DataType="{x:Type viewModels:MyViewModelInterface}">
    <local:MyView />
</DataTemplate>

Then, add one more resource for the InheritanceDataTemplateSelector:

<myWpf:InterfaceDataTemplateSelector x:Key="InterfaceDataTemplateSelector" />

Then, at the right place which needs to make use of a DataTemplate, specify that this selector should be used. For example, in an ItemsControl:

<ItemsControl ItemsSource="{Binding SomeViewModelCollection}"
    ItemTemplateSelector="{StaticResource InterfaceDataTemplateSelector}">

Note: the ViewModel interfaces do not have to extend INotifyPropertyChanged. The concrete implementation of the ViewModel may implement it, if needed.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142