Daniel Möller
Daniel Möller

Reputation: 86600

Why does DataTrigger only work for the last element in a listbox?

I'm trying to find an answer to this question (listbox items showing either a checkbox or a radio button depending on a view model property), I came across a very weird behavior.

If I use DataTrigger to set the content of a ContentControl in the DataTemplate of an item in the list box, only the last item seems to work properly.

Why does only the last item get a checkbox/radio button?

Here is the XAML where the window view model property AllowMultiItem defines if I want checkboxes or radio buttons:

<Window x:Class="TestSpace.MyWindow"
        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:TestSpace"
        mc:Ignorable="d"
        Title="MyWindow" Height="450" Width="800">
  <Grid>

    <!-- the list box -->
    <ListBox x:Name="MyList" ItemsSource="{Binding Items}">
      <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type local:MyListItem}">
          <StackPanel Orientation="Horizontal">

            <!-- this content control selects between check and radio-->
            <ContentControl>
              <ContentControl.Style>
                <Style TargetType="ContentControl">
                  <Style.Triggers>

                    <!-- trigger that creates checkbox -->
                    <DataTrigger Value="True" 
                                 Binding="{Binding Path=DataContext.AllowMultiItem, ElementName=MyList}">
                      <Setter Property="Content">
                        <Setter.Value>
                          <CheckBox IsChecked="{Binding IsChecked}"/>
                        </Setter.Value>
                      </Setter>
                    </DataTrigger>

                    <!-- trigger that creates radio button -->
                    <DataTrigger Value="False" 
                                 Binding="{Binding Path=DataContext.AllowMultiItem, ElementName=MyList}">
                      <Setter Property="Content">
                        <Setter.Value>
                          <RadioButton IsChecked="{Binding IsChecked}" GroupName="RadioGroup"/>
                        </Setter.Value>
                      </Setter>
                    </DataTrigger>


                  </Style.Triggers>
                </Style>
              </ContentControl.Style>
            </ContentControl>
                        
            <!-- item text -->
            <TextBlock Text="{Binding Text}"/>

          </StackPanel>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Grid>
</Window>

When I do this, either when AllowMultiItem is true or false, only the last item in the list box receives a checkbox/radio button.

Why does this happen?? How can I solve it?

Other code - may not be relevant

Not sure it's useful, but if you want to inspect the window view model and the items view model, or if you want to fully reproduce my test, the codes are below. I made this test the simplest possible so no unrelated bugs affect the result.

Window view model (with static function to open window):

// didn't feel the need for INotifyPropertyChanged 
// since nothing here is supposed to change
public class MyViewModel 
{
    public List<MyListItem> Items { get; private set; }
    public bool AllowMultiItem { get; private set; }

    public MyViewModel(List<MyListItem> items, bool allowMultiItem)
    {
        Items = items;
        AllowMultiItem = allowMultiItem;
    }

    public static void ShowWindow(bool allowMultiItem)
    {
        // just creating items with names from 1 to 10
        List<MyListItem> items = Enumerable.Range(1, 10)
            .Select(index=> new MyListItem("Item " + index.ToString()))
            .ToList();

        // create view model
        MyViewModel vm = new MyViewModel(items, allowMultiItem);

        // create and show window
        MyWindow window = new MyWindow();
        window.DataContext = vm;
        window.ShowDialog();
    }
}

Item view model:

public class MyListItem : INotifyPropertyChanged
{
    private bool _checked;
    private string _text;

    public event PropertyChangedEventHandler PropertyChanged;

    public string Text { get { return _text; } set { SetText(value); } }
    public bool IsChecked { get { return _checked; } set { SetChecked(value); } }

    public MyListItem(string text)
    {
        SetText(text);
        SetChecked(false);
    }

    private void SetChecked(bool value)
    {
        if (value == _checked) return;

        _checked = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsChecked"));
    }

    private void SetText(string value)
    {
        if (value == _text) 
            return;

        _text = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Text"));
    }
}

Test cases simply call MyViewModel.ShowWindow(true) and MyViewModel.ShowWindow(false)

A few things I tried

Upvotes: -1

Views: 73

Answers (2)

EldHasp
EldHasp

Reputation: 7908

Because in Content you write an INSTANCE of a RadioButton or CheckBox element. And one instance can only be shown in one visual location (frame).

Here is the correct implementation with the ability to switch to runtime.

// This is the namespace for my implementation of the ViewModelBase class.
// Since you will have your own implementation or use some kind of package,
// you need to change this `using` to the one that is relevant for you.
using Simplified;

using System.Collections.ObjectModel;

namespace Core2024.SO.Daniel_Möller.question78429551
{
    public class MyViewModel : ViewModelBase
    {
        public ObservableCollection<MyListItem> Items { get; }
        public bool AllowMultiItem { get => Get<bool>(); set => Set(value); }

        public MyViewModel(IEnumerable<MyListItem> items, bool allowMultiItem)
        {
            Items = new(items);
            AllowMultiItem = allowMultiItem;
        }

        public MyViewModel()
            : this(Enumerable.Range(1, 10).Select(index => new MyListItem("Item " + index.ToString())),
                   true)
        { }
    }

    public class MyListItem : ViewModelBase
    {

        public string Text { get => Get<string>(); set => Set(value); }
        public bool IsChecked { get => Get<bool>(); set => Set(value); }

        public MyListItem(string text)
        {
            Text = text;
        }
    }
}
<Window x:Class="Core2024.SO.Daniel_Möller.question78429551.TemplateSwitcherWindow"
        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:Core2024.SO.Daniel_Möller.question78429551"
        mc:Ignorable="d"
        Title="TemplateSwitcherWindow" Height="450" Width="800"
        DataContext="{DynamicResource vm}">
    <Window.Resources>
        <local:MyViewModel x:Key="vm"/>
        <DataTemplate x:Key="ListItem.Temlate.RadioButton"
                      DataType="{x:Type local:MyListItem}">
            <RadioButton IsChecked="{Binding IsChecked}" GroupName="RadioGroup"
                         Content="{Binding Text}"/>
        </DataTemplate>
        <DataTemplate x:Key="ListItem.Temlate.CheckBox"
                      DataType="{x:Type local:MyListItem}">
            <CheckBox IsChecked="{Binding IsChecked}"
                         Content="{Binding Text}"/>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <CheckBox Content="AllowMultiItem" IsChecked="{Binding AllowMultiItem}"/>

        <UniformGrid Grid.Row="1" Columns="2">
            <ListBox ItemsSource="{Binding Items}">
                <ListBox.Style>
                    <Style TargetType="ListBox">
                        <Setter Property="ItemTemplate" Value="{DynamicResource ListItem.Temlate.RadioButton}"/>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding AllowMultiItem}"
                                     Value="True">
                                <Setter Property="ItemTemplate" Value="{DynamicResource ListItem.Temlate.CheckBox}"/>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ListBox.Style>
            </ListBox>
            <ListBox ItemsSource="{Binding Items}"
                     ItemTemplate="{DynamicResource ListItem.Temlate.CheckBox}">
            </ListBox>
        </UniformGrid>
    </Grid>
</Window>

For your version with ContentControl it will look like this:

    <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type local:MyListItem}">
            <StackPanel Orientation="Horizontal">

                <!-- this content control selects between check and radio-->
                <ContentControl>
                    <ContentControl.Style>
                        <Style TargetType="ContentControl">
                            <Style.Triggers>

                                <!-- trigger that creates checkbox -->
                                <DataTrigger Value="True"
                                                Binding="{Binding Path=DataContext.AllowMultiItem, ElementName=MyList}">
                                    <Setter Property="ContentTemplate">
                                        <Setter.Value>
                                            <DataTemplate>
                                                <CheckBox IsChecked="{Binding IsChecked}"/>
                                            </DataTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </DataTrigger>

                                <!-- trigger that creates radio button -->
                                <DataTrigger Value="False"
                                                Binding="{Binding Path=DataContext.AllowMultiItem, ElementName=MyList}">
                                    <Setter Property="ContentTemplate">
                                        <Setter.Value>
                                            <DataTemplate>
                                                <RadioButton IsChecked="{Binding IsChecked}" GroupName="RadioGroup"/>
                                            </DataTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </DataTrigger>


                            </Style.Triggers>
                        </Style>
                    </ContentControl.Style>
                </ContentControl>

                <!-- item text -->
                <TextBlock Text="{Binding Text}"/>

            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>

Upvotes: 1

ASh
ASh

Reputation: 35646

Style is shared by multiple ContentControl, but CheckBox and RadioButton, which are defined in a trigger, are created once. That single instance is displayed in the last ContentControl.

To have unique CheckBox or RadioButton for each ContentControl, assign ContentTemplate:

<ContentControl Content="{Binding}">

  <ContentControl.Style>
    <Style TargetType="ContentControl">
      <Style.Triggers>

        <!-- trigger that creates checkbox -->
        <DataTrigger Value="True" 
                     Binding="{Binding Path=DataContext.AllowMultiItem, ElementName=MyList}">
          <Setter Property="ContentTemplate">
            <Setter.Value>
              <DataTemplate><CheckBox IsChecked="{Binding IsChecked}"/></DataTemplate>
            </Setter.Value>
          </Setter>
        </DataTrigger>

        <!-- trigger that creates radio button -->
        <DataTrigger Value="False" 
                     Binding="{Binding Path=DataContext.AllowMultiItem, ElementName=MyList}">
          <Setter Property="ContentTemplate">
            <Setter.Value>
              <DataTemplate><RadioButton IsChecked="{Binding IsChecked}" GroupName="RadioGroup"/></DataTemplate>
            </Setter.Value>
          </Setter>
        </DataTrigger>


      </Style.Triggers>
    </Style>
  </ContentControl.Style>
</ContentControl>

Upvotes: 1

Related Questions