WPF中DataTrigger仅对ListBox最后一项生效的原因及解决办法
在实现ListBox项根据ViewModel的AllowMultiItem属性动态显示CheckBox或RadioButton时,遇到异常:无论AllowMultiItem为true还是false,只有ListBox的最后一项能正常显示对应的控件,其余项均不显示。
相关XAML代码如下:
<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>
补充的ViewModel代码:
窗口ViewModel:
// 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(); } }
项ViewModel:
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")); } }
已尝试的方法:
- 为MyViewModel添加INotifyPropertyChanged并在OnContentRendered后触发AllowMultiItem变更,无效果
- 将DataTrigger绑定改为RelativeSource指向Window,此时仅第一项能显示控件,但仅第一项生效
原因分析
问题出在Style中的Setter直接创建UI元素的方式上:WPF的Style是共享资源,当你在Style的Setter.Value中直接定义CheckBox或RadioButton时,这些UI元素是单一实例。ListBox的ItemTemplate会复用这个Style,而一个UI元素只能属于一个VisualTree,所以最后只有最后一个ListBoxItem能成功将这个单一实例的控件加入自己的VisualTree,前面的项都会因为控件已经被占用而无法显示。
解决方案
有两种常用的解决方式:
方式一:使用DataTemplate作为Content的值
将CheckBox和RadioButton分别定义为DataTemplate,在Setter中设置ContentTemplate而非直接设置Content,这样每个项都会创建独立的控件实例:
<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"> <Window.Resources> <!-- 定义CheckBox的DataTemplate --> <DataTemplate x:Key="CheckBoxTemplate"> <CheckBox IsChecked="{Binding IsChecked}"/> </DataTemplate> <!-- 定义RadioButton的DataTemplate --> <DataTemplate x:Key="RadioButtonTemplate"> <RadioButton IsChecked="{Binding IsChecked}" GroupName="RadioGroup"/> </DataTemplate> </Window.Resources> <Grid> <ListBox x:Name="MyList" ItemsSource="{Binding Items}"> <ListBox.ItemTemplate> <DataTemplate DataType="{x:Type local:MyListItem}"> <StackPanel Orientation="Horizontal"> <ContentControl> <ContentControl.Style> <Style TargetType="ContentControl"> <Style.Triggers> <DataTrigger Value="True" Binding="{Binding Path=DataContext.AllowMultiItem, ElementName=MyList}"> <Setter Property="ContentTemplate" Value="{StaticResource CheckBoxTemplate}"/> </DataTrigger> <DataTrigger Value="False" Binding="{Binding Path=DataContext.AllowMultiItem, ElementName=MyList}"> <Setter Property="ContentTemplate" Value="{StaticResource RadioButtonTemplate}"/> </DataTrigger> </Style.Triggers> </Style> </ContentControl.Style> <!-- 设置Content为当前项的ViewModel,让DataTemplate绑定生效 --> <ContentControl.Content> <Binding /> </ContentControl.Content> </ContentControl> <TextBlock Text="{Binding Text}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
方式二:使用DataTemplateSelector
自定义DataTemplateSelector,根据AllowMultiItem属性选择对应的模板:
1. 创建TemplateSelector类
public class SelectionTemplateSelector : DataTemplateSelector { public DataTemplate CheckBoxTemplate { get; set; } public DataTemplate RadioButtonTemplate { get; set; } public override DataTemplate SelectTemplate(object item, DependencyObject container) { // 获取ListBox的DataContext(即MyViewModel) var listBox = FindAncestor<ListBox>(container); if (listBox?.DataContext is MyViewModel vm) { return vm.AllowMultiItem ? CheckBoxTemplate : RadioButtonTemplate; } return base.SelectTemplate(item, container); } // 查找父级元素的辅助方法 private T FindAncestor<T>(DependencyObject current) where T : DependencyObject { do { if (current is T ancestor) return ancestor; current = VisualTreeHelper.GetParent(current); } while (current != null); return null; } }
2. 在XAML中使用Selector
<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"> <Window.Resources> <DataTemplate x:Key="CheckBoxTemplate"> <CheckBox IsChecked="{Binding IsChecked}"/> </DataTemplate> <DataTemplate x:Key="RadioButtonTemplate"> <RadioButton IsChecked="{Binding IsChecked}" GroupName="RadioGroup"/> </DataTemplate> <local:SelectionTemplateSelector x:Key="SelectionTemplateSelector" CheckBoxTemplate="{StaticResource CheckBoxTemplate}" RadioButtonTemplate="{StaticResource RadioButtonTemplate}"/> </Window.Resources> <Grid> <ListBox x:Name="MyList" ItemsSource="{Binding Items}"> <ListBox.ItemTemplate> <DataTemplate DataType="{x:Type local:MyListItem}"> <StackPanel Orientation="Horizontal"> <ContentControl ContentTemplateSelector="{StaticResource SelectionTemplateSelector}" Content="{Binding}"/> <TextBlock Text="{Binding Text}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>
内容的提问来源于stack exchange,提问作者Daniel Müller
相关产品推荐
相关产品推荐

