Reputation: 86600
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
INotifyPropertyChanged
to the MyViewModel
and fire the change to AllowMultiItem
after OnContentRendered
- nothing changedDataTrigger
bindings to RelativeSource
targeting the Window: curiously, now the first item gets the box, but only the firstUpvotes: -1
Views: 73
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
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