如何在WPF中实现复选框列表框?

26

虽然我有一些编写Winforms应用程序的经验,但是WPF的“模糊性”仍然使我难以理解最佳实践和设计模式。

尽管我在运行时填充了列表,但我的列表框显示为空。

我按照这篇有帮助的文章中的简单说明进行了操作,但没有成功。我怀疑我错过了某种DataBind()方法,我需要告诉列表框我已完成修改底层列表的操作。

在我的MainWindow.xaml中,我有:

    <ListBox ItemsSource="{Binding TopicList}" Height="177" HorizontalAlignment="Left" Margin="15,173,0,0" Name="listTopics" VerticalAlignment="Top" Width="236" Background="#0B000000">
        <ListBox.ItemTemplate>
            <HierarchicalDataTemplate>
                <CheckBox Content="{Binding Name}" IsChecked="{Binding IsChecked}"/>
            </HierarchicalDataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

在我的代码后台中,我有以下代码:

    private void InitializeTopicList( MyDataContext context )
    {
        List<Topic> topicList = ( from topic in context.Topics select topic ).ToList();

        foreach ( Topic topic in topicList )
        {
            CheckedListItem item = new CheckedListItem();
            item.Name = topic.DisplayName;
            item.ID = topic.ID;
            TopicList.Add( item );
        }
    }

通过跟踪,我知道它正在被四个项目填充。

编辑

我已经将TopicList更改为ObservableCollection。但仍然无法正常工作。

    public ObservableCollection<CheckedListItem> TopicList;
编辑 #2

我做了两个改变来帮助:

在 .xaml 文件中:

ListBox ItemsSource="{Binding}"
在我填充列表后的源代码中:
listTopics.DataContext = TopicList;

我已经得到了一个列表,但在刷新时复选框状态没有自动更新。我怀疑继续深入阅读我的代码将解决这个问题。


6
Downvoter: 这个问题到底没有显示研究努力、不清楚或没有用处呢? - Bob Kaufman
6个回答

12

假设 TopicList 不是一个 ObservableCollection<T>,因此当你添加项目时,没有触发 INotifyCollection changed 来告知绑定引擎更新值。

将你的 TopicList 改为 ObservableCollection<T>,这将解决当前的问题。你也可以预先填充 List<T>,然后通过单向绑定实现数据绑定;但是,ObservableCollection<T> 是一种更健壮的方法。

编辑:

你的 TopicList 需要是一个属性,而不是成员变量;绑定需要属性。它不需要是一个 DependencyProperty

编辑 2:

修改你的 ItemTemplate,它不需要是一个 HierarchicalDataTemplate

   <ListBox.ItemTemplate>
     <DataTemplate>
       <StackPanel>
         <CheckBox Content="{Binding Name}" IsChecked="{Binding IsChecked}"/>
       </StackPanel>
     </DataTemplate>
   </ListBox.ItemTemplate>

只有在第一次绑定之前设置了值,或者容器类实现了 INotifyPropertyChanged 并在 TopicList.set 方法上调用了 PropertyChanged(...),才不需要使用 DependencyProperty。 - Mykola Bohdiuk
你应该绑定到 ListBoxItem 上的 IsSelected 属性:<CheckBox IsChecked="{Binding IsSelected, Mode=OneWay, RelativeSource={RelativeSource AncestorType=ListBoxItem, Mode=FindAncestor}}" 你也应该将 IsHitTestVisibleIsFocusable 设置为 false。 - Wouter

5

使用ObservableCollection<Topic>替代List<Topic>

编辑

它实现了INotifyCollectionChanged接口,以便WPF知道何时添加/删除/修改项目

编辑2

由于在代码中设置了TopicList,因此它应该是一个依赖属性,而不是普通字段

    public ObservableCollection<CheckedListItem> TopicList {
        get { return (ObservableCollection<CheckedListItem>)GetValue(TopicListProperty); }
        set { SetValue(TopicListProperty, value); }
    }
    public static readonly DependencyProperty TopicListProperty =
        DependencyProperty.Register("TopicList", typeof(ObservableCollection<CheckedListItem>), typeof(MainWindow), new UIPropertyMetadata(null));

编辑 3

查看项目中的更改

  1. CheckedListItem类中实现INotifyPropertyChanged接口(每个setter都应该调用PropertyChanged(this, new PropertyChangedEventArgs(<属性名称作为字符串>))事件)
  2. 或者从DependencyObject派生CheckedListItem,并将NameIDIsChecked转换为依赖属性
  3. 或者完全更新它们(topicList[0] = new CheckedListItem() { Name = ..., ID = ... }

2
感谢所有帮助我拼凑出解决方案的人。正是你们的INotifyPropertyChanged建议引导我查阅了这篇文章:http://msdn.microsoft.com/en-us/library/ms229614.aspx,最终完成了解决方案。 - Bob Kaufman
ObservableCollection<Topic> 会检测到添加/删除的项目,但不会检测到修改的项目。 - Alexey Khrenov

3

首先,你不需要使用HeirarchicalDataTemplate。像Aaron提供的普通DataTemplate就足够了。 然后,在类的构造函数中实例化TopicList ObservableCollection,这样即使在添加数据之前,ObservableCollection也会存在,并且绑定系统知道该集合的存在。当你添加每个主题/CheckedListItem时,它将自动显示在UI中。

TopicList = new ObservableCollection<CheckedListItem>(); //This should happen only once

private void InitializeTopicList( MyDataContext context )
{
    TopicList.Clear();

    foreach ( Topic topic in topicList )
    {
        CheckedListItem item = new CheckedListItem();
        item.Name = topic.DisplayName;
        item.ID = topic.ID;
        TopicList.Add( item );
    }
}

是的,这样你就不需要 TopicList 依赖属性了。 - Mykola Bohdiuk

3

已经有其他人提出了一些有用的建议(使用可观察集合以获得列表更改通知,将集合作为属性而不是字段)。 这里有两个他们没有提到的:

1)每当您在数据绑定方面遇到问题时,请查看“输出”窗口,以确保您没有收到任何绑定错误。如果您不这样做,您可能会花费很多时间来尝试修复错误的问题。

2)了解更改通知在绑定中所起的作用。除非数据源实现更改通知,否则您的数据源中的更改不会传播到UI中。对于普通属性,有两种方法可以实现更改通知:使数据源派生自DependencyObject并使绑定的属性成为依赖属性,或使数据源实现INotifyPropertyChanged并在属性值更改时引发PropertyChanged事件。当将ItemsControl绑定到集合时,请使用实现INotifyCollectionChanged的集合类(例如ObservableCollection<T>),以便集合内容和顺序的更改将传播到绑定控件。(请注意,如果您想要将集合中的项的更改传播到绑定的控件,则这些项目也需要实现更改通知。)


1

我知道这是一个很老的问题,但我想创建一个自定义ListBox,其中包含内置的全选/取消全选功能,可以获取所选项目。

enter image description here

自定义ListBox

public class CustomListBox : ListBox
    {
        #region Constants

        public static new readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(CustomListBox), new PropertyMetadata(default(IList), OnSelectedItemsPropertyChanged));

        #endregion

        #region Properties

        public new IList SelectedItems
        {
            get => (IList)GetValue(SelectedItemsProperty);
            set => SetValue(SelectedItemsProperty, value);
        }

        #endregion

        #region Event Handlers

        private static void OnSelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((CustomListBox)d).OnSelectedItemsChanged((IList)e.OldValue, (IList)e.NewValue);
        }

        protected virtual void OnSelectedItemsChanged(IList oldSelectedItems, IList newSelectedItems)
        {
        }

        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            SetValue(SelectedItemsProperty, base.SelectedItems);
        }

        #endregion
    }

ListBoxControl.cs

public partial class ListBoxControl : UserControl { #region 常量

public static new readonly DependencyProperty ContentProperty =
                    DependencyProperty.Register(nameof(Content), typeof(object), typeof(ListBoxControl),
                        new PropertyMetadata(null));

public static new readonly DependencyProperty ContentTemplateProperty =
                    DependencyProperty.Register(nameof(ContentTemplate), typeof(DataTemplate), typeof(ListBoxControl),
                         new PropertyMetadata(null));

public static readonly DependencyProperty ItemsProperty =
                    DependencyProperty.Register(nameof(Items), typeof(IList), typeof(ListBoxControl),
                         new PropertyMetadata(null));

public static readonly DependencyProperty SelectedItemsProperty =
                    DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(ListBoxControl),
                       new UIPropertyMetadata(null, OnSelectedItemsChanged));

#endregion

#region Properties

public new DataTemplate ContentTemplate
{
    get => (DataTemplate)GetValue(ContentTemplateProperty);
    set => SetValue(ContentTemplateProperty, value);
}

public IList Items
{
    get => (IList)GetValue(ItemsProperty);
    set => SetValue(ItemsProperty, value);
}

public IList SelectedItems
{
    get => (IList)GetValue(SelectedItemsProperty);
    set => SetValue(SelectedItemsProperty, value);
}

#endregion

#region Constructors

public ListBoxControl()
{
    InitializeComponent();
}

#endregion

#region Event Handlers

private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (d is not ListBoxControl || e.NewValue is not IList newValue)
    {
        return;
    }
     
    var mylist = (d as ListBoxControl).CustomList;

    foreach (var selectedItem in newValue)
    {
        mylist.UpdateLayout();
        if (mylist.ItemContainerGenerator.ContainerFromItem(selectedItem) is ListBoxItem selectedListBoxItem)
        {
            selectedListBoxItem.IsSelected = true;
        }
    }
}

#endregion

#region Private Methods

private void CheckAll_Click(object sender, RoutedEventArgs e)
{
    CustomList.SelectAll();
}

private void UncheckAll_Click(object sender, RoutedEventArgs e)
{
    CustomList.UnselectAll();
}

#endregion

}

#endregion

ListBoxControl.xaml

<UserControl x:Class="UserControls.ListBoxControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:UserControls"
             xmlns:str="Client.Properties"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             x:Name="this">

    <UserControl.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
    </UserControl.Resources>
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <local:CustomListBox  x:Name="CustomList" 
                             Grid.Row="0"   
                             Width="250"
                             HorizontalAlignment="Left"
                             SelectionMode="Multiple"
                             Visibility="Visible"
                             MinHeight="25"
                             MaxHeight="400"
                             ItemsSource="{Binding  ElementName=this, Path =Items}"
                             SelectedItems="{Binding  ElementName=this, Path =SelectedItems,Mode=TwoWay}"
                             Style="{StaticResource {x:Type ListBox}}"
                             ScrollViewer.VerticalScrollBarVisibility="Auto">
            <local:CustomListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True" >
                            <Setter Property="FontWeight" Value="Bold" />
                            <Setter Property="Background" Value="Transparent" />
                            <Setter Property="BorderThickness" Value="0" />
                        </Trigger>
                        <Trigger Property="IsMouseCaptureWithin" Value="true">
                            <Setter Property="IsSelected" Value="true" />
                        </Trigger>
                        <Trigger Property="IsMouseCaptureWithin" Value="False">
                            <Setter Property="IsSelected" Value="False" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </local:CustomListBox.ItemContainerStyle>

            <local:CustomListBox.ItemTemplate>
                <DataTemplate>
                    <DockPanel>
                        <CheckBox Margin="4" IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}},Path=IsSelected}" />
                        <ContentPresenter Content="{Binding .}" ContentTemplate="{Binding ElementName=this, Path = ContentTemplate, Mode=OneWay}"/>
                    </DockPanel>
                </DataTemplate>
            </local:CustomListBox.ItemTemplate>
        </local:CustomListBox>
        <Grid Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" >
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="250" />
            </Grid.ColumnDefinitions>
            <StackPanel Grid.Row="0" Grid.Column="1"
                            Orientation="Horizontal"  
                            HorizontalAlignment="Left">
                <Button Click="CheckAll_Click"
                        BorderBrush="Transparent"
                        ToolTip="Check all">
                    <Button.Content>
                        <Image Source="CheckAll.png" Height="16" Width="16"/>
                    </Button.Content>
                </Button>

                <Button         
                        Click="UncheckAll_Click"
                        BorderBrush="Transparent"
                        Visibility="Visible"
                        ToolTip="Unchecked all">
                    <Button.Style>
                        <Style TargetType="Button">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding ElementName=this, Path = SelectedItems.Count}" Value="0">
                                    <Setter Property="Button.Visibility" Value="Collapsed" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>
                    <Button.Content>
                        <Image Source="UncheckAll.png" Height="16" Width="16" />
                    </Button.Content>
                </Button>

            </StackPanel>

            <TextBlock  Grid.Row="0" Grid.Column="1"
                       Text="{Binding ElementName=this, Path = SelectedItems.Count, StringFormat={x:Static str:Resources.STE_LABEL_X_ITEMS_CHECKED}, Mode=OneWay}"
                        HorizontalAlignment="Right" TextAlignment="Right"  VerticalAlignment="Center"
                        Foreground="White" />
        </Grid>


    </Grid>
</UserControl>

现在您可以在任何控件或页面中使用自定义控件,并传递任何您想要的内容。 例如:ConfigView.xaml
<UserControl ..
 xmlns:userControls="Client.UserControls"
..>

<userControls:ListBoxControl 
                    ShowCheckBox="True" 
                    MinHeight="25"
                    MaxHeight="400"
                    ScrollViewer.VerticalScrollBarVisibility="Auto"
                    Items="{Binding MyLists, Mode=OneWay}"
                    SelectedItems="{Binding SelectedMyLists,Mode=TwoWay}"
                    HorizontalAlignment="Left">
                    <userControls:ListBoxControl.ContentTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal" >
                                <Image Source="{Binding Icon}"/>
                                <TextBlock VerticalAlignment="Center" Text="{Binding Name,StringFormat=' {0}'}" />
                            </StackPanel>
                        </DataTemplate>
                    </userControls:ListBoxControl.ContentTemplate>
                </userControls:ListBoxControl>

在这里,我们将选择的项目绑定到我们的模型ConfigViewViewModel,并进行显式转换。
private IList _myLists;
 public IList MyLists
        {
            get => _myLists;
            set
            {
                if (_myLists == value)
                {
                    return;
                }

                _myLists = value;
                OnPropertyChanged(nameof(SelectedItems));
            }
        }
public IEnumerable<MyModel> SelectedItems => MyLists.Cast<MyModel>();

0

将你的绑定更改为

 <ListBox ItemsSource="{Binding Path=TopicList}"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接