0
votes

I need to add different controls (TextBox/CheckBox/ComboBox, etc) in ItemsControl based on certain condition. Each Item in ItemsControl is a Name-Value pair. Name is always represented by TextBlock but Value can be any UI control. I am using horizontally aligned StackPanel to represent each Item. First control in StackPanel remains TextBlock but second control is dependent upon "ItemDataType" property set in ViewModel at runtime.

The problem I have is that I am not able to assign different controls in StackPanel's 2nd element using Style trigger with ItemDataType property.

Code Snippet:

<UserControl.Resources>

    <DataTemplate x:Key="TextBoxTemplate">
        <TextBox Text="{Binding Path=DataValue}"/>
    </DataTemplate>

    <DataTemplate x:Key="ComboBoxTemplate">
        <ComboBox ItemsSource="{Binding Path=SelectionList}" SelectedValue="{Binding Path=DataValue,Mode=TwoWay}"/>
    </DataTemplate>

    <DataTemplate x:Key="CheckBoxTemplate">
        <CheckBox IsChecked="{Binding Path=DataValue,Mode=TwoWay}" />
    </DataTemplate>

    <DataTemplate x:Key="ButtonTemplate">
        <Button Content="{Binding Path=DataValue}"/>
    </DataTemplate>

    <DataTemplate x:Key="dynamicTemplate">
        <StackPanel Orientation="Horizontal" Tag="{Binding ItemDataType}">
            <TextBlock Text="{Binding Path=DataName,Mode=TwoWay}"/>
            <ContentControl>
                <ContentControl.Style>
                    <Style TargetType="{x:Type ContentControl}">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding ItemDataType}" Value="TextBox">
                                <Setter Property="Template" Value="{StaticResource TextBoxTemplate}"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ContentControl.Style>
            </ContentControl>
        </StackPanel>
    </DataTemplate>

</UserControl.Resources>

<Grid>
    <!-- CONTROL LAYOUT -->
    <ItemsControl ItemsSource="{Binding Path=DataList,Mode=TwoWay}" ItemTemplate="{StaticResource dynamicTemplate}">

        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel></StackPanel>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</Grid>

Error I get is DataTemplate invalid for ContentControl.Template property. I understand that what I am doing is wrong, but I want help to do it right way.

Thanks,

RDV

2

2 Answers

1
votes

The answer (or an answer) is to write a DataTemplateSelector which returns the correct template based on some arbitrary parameter. Something like the following (excuse all the null-checking boilerplate noise; this is copied directly out of production code).

/// <summary>
/// Selects template based on the value of a named property of the data item. 
/// Property name is specified by ResourceKeyPropertyName.
/// </summary>
public class PropertyValueTemplateSelector : DataTemplateSelector
{
    /// <summary>
    /// Gets or sets a path to a value on the source object to serve as a resource key for 
    /// the DataTemplate used to display the source object.
    /// </summary>
    public string ResourceKeyPropertyName { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var context = container as FrameworkElement;
        DataTemplate template = null;

        if (null == container)
        {
            throw new NullReferenceException("container");
        }
        else if (null == context)
        {
            throw new Exception("container must be FramekworkElement");
        }
        else if (String.IsNullOrEmpty(ResourceKeyPropertyName))
        {
            throw new NullReferenceException("ResourceKeyPropertyName");
        }
        else if (null == item)
        {
            return null;
        }

        var prop = item.GetType().GetProperty(ResourceKeyPropertyName);

        if (null == prop)
        {
            throw new Exception("Undefined property " + ResourceKeyPropertyName);
        }

        var resourceKey = prop.GetValue(item, null);

        if (null != resourceKey)
        {
            try
            {
                template = context.FindResource(resourceKey) as DataTemplate;
            }
            catch (Exception ex)
            {
                Ability.CAPS.WPF.Utilities.ErrorHandler.HandleException(ex, Ability.Logging.AbilityExceptionPolicy.GeneralExceptionPolicy);
                template = null;
            }
        }

        return template ?? base.SelectTemplate(item, container);
    }
}

Use like so in XAML:

<ItemsControl
    >
    <ItemsControl.ItemTemplateSelector>
        <!-- Tell it use the value of the "DataName" property as the 
             resource key for the template it uses.
        -->
        <local:PropertyValueTemplateSelector 
            ResourceKeyPropertyName="DataName" />
    </ItemsControl.ItemTemplateSelector>
    <!-- etc. 
         etc. 
         etc. -->
</ItemsControl>

If an item's DataName property is "Foo", this will look in the local context for a DataTemplate whose resource key is "Foo" and use that -- so name your DataTemplates accordingly and go to town. There's no simpler or more generalized way of doing this. By the way, in our code, this is used in only one place at the moment (I just wrote it last month), and it's using enum values for resource keys rather than strings. Any object will do. Resource keys don't have to be strings.

Alternatively, you might want to have a lookup or something to translate DataName values to a different set of keys. Your call, if that better suits your needs.

Another option would be having one item template with a set of Triggers that sets a template on an inner ContentControl depending on the value of DataName. Here's an answer that does something like that.

1
votes

I wanted to have a XAML solution - took me sometime :-). Below is the working code:

    <Style x:Key="nvpTextBlockStyle" TargetType="{x:Type TextBlock}" BasedOn="{StaticResource {x:Type TextBlock}}">
        <Setter Property="HorizontalAlignment" Value="Left"/>
        <Setter Property="Width" Value="{Binding Path=LabelWidthStr, FallbackValue=50}"/>
        <Setter Property="Margin" Value="0,5,0,5"/>
        <Setter Property="Text" Value="{Binding Path=NameData,Mode=TwoWay}"/>
        <Setter Property="FontSize" Value="16"/>
    </Style>

    <DataTemplate x:Key="textBoxTemplate">
        <TextBox Margin="1,1" Text="{Binding Path=ValueData,UpdateSourceTrigger=PropertyChanged,
                    ValidatesOnExceptions=True,NotifyOnValidationError=True,ValidatesOnDataErrors=True}"/>
    </DataTemplate>

    <DataTemplate x:Key="comboBoxTemplate">
        <ComboBox HorizontalAlignment="Left" ItemsSource="{Binding Path=SelectionList}" 
                      SelectedValue="{Binding Path=ValueData,Mode=TwoWay}"
                      IsEnabled="{Binding IsDataItemEnabled}"/>
    </DataTemplate>

    <DataTemplate x:Key="checkBoxTemplate">
        <CheckBox HorizontalAlignment="Left" VerticalAlignment="Center"  
                      IsChecked="{Binding Path=ValueData,Mode=TwoWay}"/>
    </DataTemplate>

    <DataTemplate x:Key="buttonTemplate">
        <Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}, Path=DataContext.AddCommand}" 
                    CommandParameter="{Binding}" Width="30" Height="25">
            <TextBlock Text="&#x1F511;" FontFamily="Segoe UI Symbol" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="Gray" />
        </Button>
    </DataTemplate>

    <DataTemplate x:Key="dynamicTemplate">
        <StackPanel Orientation="Horizontal" Margin ="5">
            <TextBlock Style="{StaticResource nvpTextBlockStyle}"/>
            <ContentPresenter Content="{Binding}" 
                Tag="{Binding Path=CustomDataType, FallbackValue={x:Static local:CustomViewModel.TEXTBOX_TEMPLATE}}">
                <ContentPresenter.Resources>
                    <Style TargetType="{x:Type ContentPresenter}">
                        <Style.Triggers>
                            <Trigger Property="Tag" Value="{x:Static local:CustomViewModel.TEXTBOX_TEMPLATE}">
                                <Setter Property="ContentTemplate" Value="{StaticResource textBoxTemplate}"/>
                            </Trigger>
                            <Trigger Property="Tag" Value="{x:Static local:CustomViewModel.COMBOBOX_TEMPLATE}">
                                <Setter Property="ContentTemplate" Value="{StaticResource comboBoxTemplate}"/>
                            </Trigger>
                            <Trigger Property="Tag" Value="{x:Static local:CustomViewModel.CHECKBOX_TEMPLATE}">
                                <Setter Property="ContentTemplate" Value="{StaticResource checkBoxTemplate}"/>
                            </Trigger>
                            <Trigger Property="Tag" Value="{x:Static local:CustomViewModel.BUTTON_TEMPLATE}">
                                <Setter Property="ContentTemplate" Value="{StaticResource buttonTemplate}"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </ContentPresenter.Resources>
            </ContentPresenter>
        </StackPanel>
    </DataTemplate>

    <Grid>       
    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <ItemsControl  ItemsSource="{Binding Path=CustomDataList,Mode=TwoWay}" 
                       ItemTemplate="{StaticResource dynamicTemplate}";
                       KeyboardNavigation.IsTabStop="False">
            <ItemsPanelTemplate>
                <StackPanel></StackPanel>
            </ItemsPanelTemplate>
        </ItemsControl>
    </ScrollViewer>

</Grid>

Thanks,

RDV