65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中的多选 ComboBox

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (46投票s)

2013年3月18日

CPOL

9分钟阅读

viewsIcon

302763

downloadIcon

20059

逐步创建多选组合框用户控件。

引言

最近,在我们的项目中,我们希望允许用户在列表中选择多个值。但列表应该填充在网格行内。所以我们不想使用列表框,我们也不对第三方工具感兴趣。取而代之的是,我们想使用一个多选组合框。当我浏览各种博客、论坛等时,我得到了一些不错的代码,但没有一个代码与 MVVM 模式一起工作。在那些文章中,大多数数据源绑定都是在代码隐藏中完成的。所以我对现有的代码进行了一些更改以支持 MVVM。在本文中,我将逐步解释如何创建多选组合框用户控件。本文还将帮助最近开始学习 WPF 的人,因为我解释了如何创建样式和依赖属性。

Using the Code

创建一个新的 WPF 用户控件库。将用户控件重命名为 MultiSelectComboBox

为了创建多选组合框,我们需要分析构建这样一个控件需要什么。我们需要一个组合框,并且组合框下拉列表中的每个项目都需要添加一个复选框。由于我们将为组合框项目编写自定义数据模板,因此我们不能直接使用组合框。我们可以做的是,除了定义项目模板之外,还要精心设计组合框的模板。

我们的组合框应该看起来像下面的样子

为了实现这一点,正如我在上图中所提到的,我们需要一个切换按钮来确定是打开/关闭下拉列表,以及显示选定的值。我们需要一个弹出控件,我们在其中显示所有带有复选框的项目。所有这些组合在一起就构成了我们的自定义多选组合框控件。

我现在将把文章分成三个部分

  1. 定义样式和创建 XAML 文件
  2. 在用户控件的代码隐藏中添加依赖属性和其他对象
  3. 在其他 XAML 应用程序中使用此多选组合框 DLL

定义样式和模板

删除网格标签并在用户控件中添加组合框。

<ComboBox
        x:Name="MultiSelectCombo"  
        SnapsToDevicePixels="True"
        OverridesDefaultStyle="True"
        ScrollViewer.HorizontalScrollBarVisibility="Auto"
        ScrollViewer.VerticalScrollBarVisibility="Auto"
        ScrollViewer.CanContentScroll="True"
        IsSynchronizedWithCurrentItem="True"
             >

为上述组合框添加一个项目模板。项目模板应该是一个复选框。将复选框的内容属性绑定到某个属性 Title。请记住,我们还没有开始编写代码隐藏。我们必须在代码隐藏中设置此属性。将复选框的 Ischecked 属性绑定到组合框的 IsSelected 属性。

<ComboBox.ItemTemplate>
    <DataTemplate>
        <CheckBox Content="{Binding Title}"
                  IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
                  Tag="{RelativeSource FindAncestor, 
                  AncestorType={x:Type ComboBox}}"
                 
                  />
    </DataTemplate>
</ComboBox.ItemTemplate>

在组合框的控件模板中添加一个网格,并包含一个切换按钮和一个弹出窗口,如下所示

<ComboBox.Template>
    <ControlTemplate TargetType="ComboBox">              
        <Grid >
             <ToggleButton 
                        x:Name="ToggleButton" 
                       Grid.Column="2" IsChecked="
                       {Binding Path=IsDropDownOpen,Mode=TwoWay,
                       RelativeSource={RelativeSource TemplatedParent}}"
                        Focusable="false"                           
                        ClickMode="Press" HorizontalContentAlignment="Left" >
                        <ToggleButton.Template>
                            <ControlTemplate>
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="*"/>
                                        <ColumnDefinition Width="18"/>
                                    </Grid.ColumnDefinitions>
                                    <Border
                  x:Name="Border" 
                  Grid.ColumnSpan="2"
                  CornerRadius="2"
                  Background="White"
                  BorderBrush="Black"
                  BorderThickness="1,1,1,1" />
                                    <Border 
                    x:Name="BorderComp" 
                  Grid.Column="0"
                  CornerRadius="2" 
                  Margin="1" 
                 Background="White"
                  BorderBrush="Black"
                  BorderThickness="0,0,0,0" >
                                        <TextBlock Text="
                                        {Binding Path=Text,RelativeSource=
                                        {RelativeSource Mode=FindAncestor, 
                                        AncestorType=UserControl}}" 
                                               Background="White" 
                                               Padding="3" />
                                    </Border>
                                    <Path 
                  x:Name="Arrow"
                  Grid.Column="1"     
                  Fill="Black"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center"
                  Data="M 0 0 L 4 4 L 8 0 Z"/>
                                </Grid>
                            </ControlTemplate>
                        </ToggleButton.Template>
                         </ToggleButton>
            <Popup 
            Name="Popup"
            Placement="Bottom"                        
            AllowsTransparency="True" 
            Focusable="False" >
                <Grid 
                      Name="DropDown"
                      SnapsToDevicePixels="True" 
                    <Border 
                        x:Name="DropDownBorder"
                       BorderThickness="1" Background="White"
                                 BorderBrush="Black"/>
                    <ScrollViewer Margin="4,6,4,6" 
                    SnapsToDevicePixels="True" 
                    DataContext="{Binding}">
                        <StackPanel IsItemsHost="True" 
                        KeyboardNavigation.DirectionalNavigation=
                        "Contained" />
                    </ScrollViewer>
                </Grid>
            </Popup>
        </Grid>
    </ControlTemplate>
</ComboBox.Template>

我们已经添加了组合框模板,但尚未设置弹出窗口和切换按钮之间的关系。我们将在此处使用模板绑定概念。

TemplateBinding 类似于普通数据绑定,但它只能用于模板。这里切换按钮充当模板父项,我们将通过为弹出窗口的 IsOpen 属性指定模板绑定来将切换按钮的 IsChecked 属性绑定到弹出窗口。一旦您看到下面的代码,您将对其有更深入的了解。

在切换按钮中添加以下属性

IsChecked="{Binding Path=IsDropDownOpen,
Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"

在弹出窗口中添加以下代码

IsOpen="{TemplateBinding IsDropDownOpen}"
PopupAnimation="Slide"

我们使用一个通用属性 IsDropDownOpen 来设置两个对象的绑定,并在切换按钮中将其设置为双向,以便每当弹出窗口关闭时,切换按钮的 IsChecked 属性将设置为 false。此外,我们将 PopupAnimation 设置为 Slide

我们现在还需要做一件事。弹出窗口的宽度应与组合框的宽度匹配,并且下拉列表的高度也应设置。

在弹出窗口中的网格中也添加以下属性。

MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">

现在,在控件模板关闭标签之前,让我们为组合框添加一些触发器。

  1. 每当组合框中没有项目时,我们都需要为弹出窗口设置一些最小高度。
  2. <ControlTemplate.Triggers>
        <Trigger Property="HasItems" Value="false">
            <Setter TargetName="DropDownBorder" 
            Property="MinHeight" Value="95"/>
        </Trigger>                    
    </ControlTemplate.Triggers>
  3. 为下拉列表弹出窗口设置一些圆角半径
  4. <Trigger SourceName="Popup" 
    Property="Popup.AllowsTransparency" Value="true">
        <Setter TargetName="DropDownBorder" 
        Property="CornerRadius"   Value="4"/>
        <Setter TargetName="DropDownBorder" 
        Property="Margin" Value="0,2,0,0"/>
    </Trigger>

现在我们基本完成了 XAML 页面。如果需要任何样式更改,我们将返回 XAML。

第二步是在代码隐藏中添加依赖属性。大多数 WPF 开发人员已经了解依赖属性。但对于初学者来说,依赖属性是一种特殊的属性,当我们调用 getvalue() 方法时,它会动态地从依赖对象获取值。当我们为依赖属性设置值时,它不会存储在对象中的字段里,而是存储在依赖对象基类提供的键的字典中

我将添加四个依赖属性

  • ItemSource
  • SelectedItems
  • DefaultText
  • 文本

这四个属性就足够了,但如果您需要任何其他依赖属性,您可以添加自己的。

public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(Dictionary<string, object>), 
            typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));

public static readonly DependencyProperty SelectedItemsProperty =
   DependencyProperty.Register("SelectedItems", 
   typeof(Dictionary<string, object>), 
   typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public static readonly DependencyProperty TextProperty =
   DependencyProperty.Register("Text", 
   typeof(string), typeof(MultiSelectComboBox), 
   new UIPropertyMetadata(string.Empty));

public static readonly DependencyProperty DefaultTextProperty =
    DependencyProperty.Register("DefaultText", typeof(string), 
    typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));

public Dictionary<string, object> ItemsSource
{
    get { return (Dictionary<string, 
    object>)GetValue(ItemsSourceProperty); }
    set
    {
        SetValue(ItemsSourceProperty, value);
    }
 }

public Dictionary<string, object> SelectedItems
{
    get { return (Dictionary<string, 
    object>)GetValue(SelectedItemsProperty); }
    set
    {
        SetValue(SelectedItemsProperty, value);             
    }
}

public string Text
{
    get { return (string)GetValue(TextProperty); }
    set { SetValue(TextProperty, value); }
}

public string DefaultText
{
    get { return (string)GetValue(DefaultTextProperty); }
    set { SetValue(DefaultTextProperty, value); }
}

注意:我将 ItemSourceSelectedItems 属性都设置为 dictionary 对象,因为对于组合框的绑定,简单的键值对就足够了。但是您可以使用对象列表而不是 dictionary

在同一个命名空间中创建一个名为“Node”的类,其中包含两个属性

  • 标题
  • IsSelected

请记住,我已经告诉您,我们将复选框的内容属性绑定到 Title

public class Node
{
    public Node(string title)
    {
        Title = title;
    }
    
    public string Title { get; set; }
    public bool IsSelected { get; set; }
}

现在,添加一个名为 _nodelist 的类的一个 Observablecollection

private ObservableCollection<Node> _nodeList;

在上面的字段的构造函数中设置值。

_nodeList = new ObservableCollection<Node>();

添加一个名为 DisplayInControl 的方法。此方法将显示依赖属性 ItemsSource 中的项目。

private void DisplayInControl()
{
    _nodeList.Clear();
    if (this.ItemsSource.Count > 0)
        _nodeList.Add(new Node("All"));
    foreach (KeyValuePair<string, object> keyValue in this.ItemsSource)
    {
        Node node = new Node(keyValue.Key);
        _nodeList.Add(node);
    }
    MultiSelectCombo.ItemsSource = _nodeList;
}

每当有多个项目时,我们都会添加一个额外的值 All。对于 ItemsSource 中的每个项目,我们都将键添加到 nodelist。最后,我们将此 nodelist 设置为我们命名为 MultiSelectCombo 的组合框的 ItemsSource。但是我们如何使用这个方法呢?所以,在依赖属性中,我们必须设置 valuechanged 属性。像下面这样重写 ItemSource 的依赖属性

public static readonly DependencyProperty ItemsSourceProperty =
   DependencyProperty.Register("ItemsSource", typeof(Dictionary<string,
   object>), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
   new PropertyChangedCallback(MultiSelectComboBox.OnItemsSourceChanged)));

itemsSourceChanged 事件中,调用 DisplayInControl 方法。

private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    MultiSelectComboBox control = (MultiSelectComboBox)d;
    control.DisplayInControl();
}

我们已经添加了一个 PropertyChanged 事件。下一步将是每当下拉列表中选中一个项目时。我们必须将其设置为 SelectedItems,并且还必须在组合框中显示它。如果有多个值,我们必须以逗号分隔的格式显示该项目,如果选择了所有值,我们将 All 设置为下拉列表中的文本,并选中所有项目。

在复选框中添加一个 Click 事件

<CheckBox Content="{Binding Title}"
      IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
      Tag="{RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}"
      Click="CheckBox_Click" />

在代码隐藏中,添加逻辑以将 nodelist 中每个节点的 IsSelected 属性设置为 true(如果它们被选中)。

private void CheckBox_Click(object sender, RoutedEventArgs e)
{
    CheckBox clickedBox = (CheckBox)sender;

    if (clickedBox.Content == "All")
    {
        if (clickedBox.IsChecked.Value)
                {
                    foreach (Node node in _nodeList)
                    {
                        node.IsSelected = true;
                    }
                }
                else
                {
                    foreach (Node node in _nodeList)
                    {
                        node.IsSelected = false;
                    }
                }
    }
    else
    {
        int _selectedCount = 0;
        foreach (Node s in _nodeList)
        {
            if (s.IsSelected && s.Title != "All")
                _selectedCount++;
        }
        if (_selectedCount == _nodeList.Count - 1)
            _nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = true;
        else
            _nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = false;
    }
    SetSelectedItems();
}

添加以下方法来设置选定的项目

private void SetSelectedItems()
{
    if (SelectedItems == null)
        SelectedItems = new Dictionary<string, object>();
    SelectedItems.Clear();
    foreach (Node node in _nodeList)
    {
        if (node.IsSelected && node.Title != "All")
        {
            if (this.ItemsSource.Count > 0)

                SelectedItems.Add(node.Title, this.ItemsSource[node.Title]);
        }
    }
}

即使我们为每个节点都设置了 IsSelected =true,在 UI 中,当您选中“All”选项时,其他复选框仍然不会被选中。这是因为我们没有为 Node 类中的属性设置属性更改通知事件。现在我们重新访问 Node 类并实现 INotifyPropertyChanged 接口。

public class Node : INotifyPropertyChanged
{
    private string _title;
    private bool _isSelected;
    #region ctor
    public Node(string title)
    {
        Title = title;
    }
    #endregion

    #region Properties
    public string Title
    {
        get
        {
            return _title;
        }
        set
        {
            _title = value;
            NotifyPropertyChanged("Title");
        }
    }
    public bool IsSelected
    {
        get
        {
            return _isSelected;
        }
        set
        {
            _isSelected = value;
            NotifyPropertyChanged("IsSelected");
        }
    }
    #endregion

    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

现在我们的控件还剩两个任务。在组合框中显示选定的项目,并且当我们加载页面并有一些选定的值时,我们默认必须选中它们以通知用户。

下面的方法将在切换按钮内容中设置文本

private void SetText()
{
    if (this.SelectedItems != null)
    {
        StringBuilder displayText = new StringBuilder();
        foreach (Node s in _nodeList)
        {
            if (s.IsSelected == true && s.Title == "All")
            {
                displayText = new StringBuilder();
                displayText.Append("All");
                break;
            }
            else if (s.IsSelected == true && s.Title != "All")
            {
                displayText.Append(s.Title);
                displayText.Append(',');
            }
        }
        this.Text = displayText.ToString().TrimEnd(new char[] { ',' }); 
    }           
    // set DefaultText if nothing else selected
    if (string.IsNullOrEmpty(this.Text))
    {
        this.Text = this.DefaultText;
    }
}

我们必须在复选框点击事件的末尾调用上面的方法(SetText),以便每当 SelectedItems 更改时,都会调用该方法。

现在添加一个方法,根据 SelectedItems 设置每个节点的 IsSelected 属性。我们需要这个方法来预填充页面加载时的选定项目。

private void SelectNodes()
{
    foreach (KeyValuePair<string, object> keyValue in SelectedItems)
    {
        Node node = _nodeList.FirstOrDefault(i => i.Title == keyValue.Key);
        if (node != null)
            node.IsSelected = true;
    }
}

我们需要修改 SelectedItemsProperty 以包含属性更改事件,并在事件中调用 selectNodes 方法和 SetText 方法。

所以我们的依赖属性将变为

public static readonly DependencyProperty SelectedItemsProperty =
         DependencyProperty.Register
         ("SelectedItems", typeof(Dictionary<string, object>), 
         typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
         new PropertyChangedCallback
         (MultiSelectComboBox.OnSelectedItemsChanged)));

private static void OnSelectedItemsChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    MultiSelectComboBox control = (MultiSelectComboBox)d;
    control.SelectNodes();
    control.SetText();
}

现在我们已经添加了大部分基本功能,是时候在应用程序中使用该控件了。

添加一个新的 WPF 应用程序项目,并命名为 MultiSelectDemo。我在这里不解释 MVVM 模式。您可以找到关于使用 MVVM 创建简单 WPF 应用程序的各种文章。我将跳过这部分,直接带您到我们的控件实现。添加一个具有 InotifyProperyChanged 接口实现的 viewmodelbase。添加一个继承自 ViewModelBaseviewmodel,它应该有一个 Items 和一个 SelectedItems 字典属性。为了测试我们的控件,我在构造函数中添加了以下代码

Items = new Dictionary<string, object>();
            Items.Add("Chennai", "MAS");
            Items.Add("Trichy", "TPJ");
            Items.Add("Bangalore", "SBC");
            Items.Add("Coimbatore", "CBE");

            SelectedItems = new Dictionary<string, object>();
            SelectedItems.Add("Chennai", "MAS");
            SelectedItems.Add("Trichy", "TPJ");

现在将 WPF 用户控件库项目添加为该应用程序的引用,这样您就可以在此处看到用户控件库项目的命名空间。在视图中,我包含了一个对用户控件库的引用。

xmlns:control="clr-namespace:
MultiSelectComboBox;assembly=MultiSelectComboBox"

在网格内部,我添加了我们的控件,并带有以下属性

  • ItemsSource
  • SelectedItems
  • 宽度
  • 高度

请记住,我们在控件中没有设置任何 WidthHeight 属性。这是因为在不同的地方我们需要不同的宽度和高度。所以我总是建议您将其添加到视图中。

<control:MultiSelectComboBox 
Width="200" Height="30" 
  ItemsSource="{Binding Items}" 
  SelectedItems="{Binding SelectedItems}" 
  x:Name="MC" />

现在我们可以看到我们的 MultiSelectCombobox 工作正常,并且我们可以选择不同的项目。我们也可以通过绕过 MVVM 的代码隐藏来使用此控件。我还将向您展示一个如何从 XAML 文件代码隐藏中使用它的示例。我们必须从 XAML 中删除 ItemsSourceSelectedItems 属性。在代码隐藏构造函数中添加相同的测试数据。

最后,将 ItemsSourceSelectedItems 属性设置如下

public MainWindow()
{
    InitializeComponent();
     Items = new Dictionary<string, object>();
    Items.Add("Chennai", "MAS");
    Items.Add("Trichy", "TPJ");
    Items.Add("Bangalore", "SBC");
    Items.Add("Coimbatore", "CBE");

    SelectedItems = new Dictionary<string, object>();
    SelectedItems.Add("Chennai", "MAS");
    SelectedItems.Add("Trichy", "TPJ");


    MC.ItemsSource = Items;
    MC.SelectedItems = SelectedItems;
}

扩展 - 1 (工具提示)

如果组合框的宽度很小,并且您选择了多个项目,那么您将无法在文本中看到所有项目。所以我们必须设置工具提示,以便当用户将鼠标悬停在组合框上时,他/她应该能够看到所有选定的项目。在下面的代码中,我将组合框的文本绑定到工具提示。

 ToolTip="{Binding Path=Text, RelativeSource={RelativeSource Self}}" 
XAML 中的组合框控件现在看起来像这样

 <control:MultiSelectComboBox Width="100" Height="30" 
ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems}" 
x:Name="MC" ToolTip="{Binding Path=Text, RelativeSource={RelativeSource Self}}"/> 

请检查附件的演示文件。希望这份文档和附件能帮到您。请与您的朋友分享这份文档,并提供您宝贵的反馈。

 

扩展 - 2 取消选中组合框中的“All”项 

 当您单击“All”选项复选框时,该组合框中的所有项目都应被选中。但是反向场景,即取消选中“All”应该取消选中所有选定的项目,而事实并非如此。 我通过在 checkbox_click 方法中使用以下代码解决了这个问题。

 如果 clickedbox 被选中,我们需要选中所有节点,如果 clickedbox 被取消选中,我们需要取消选中所有节点。

 private void CheckBox_Click(object sender, RoutedEventArgs e)
        {
            CheckBox clickedBox = (CheckBox)sender;
            if (clickedBox.Content == "All" )
            {
                if (clickedBox.IsChecked.Value)
                {
                    foreach (Node node in _nodeList)
                    {
                        node.IsSelected = true;
                    }
                }
                else
                {
                    foreach (Node node in _nodeList)
                    {
                        node.IsSelected = false;
                    }
                }
            }
            else
            {
                int _selectedCount = 0;
                foreach (Node s in _nodeList)
                {
                    if (s.IsSelected && s.Title != "All")
                        _selectedCount++;
                }
                if (_selectedCount == _nodeList.Count - 1)
                    _nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = true;
                else
                    _nodeList.FirstOrDefault(i => i.Title == "All").IsSelected = false;
            }
            SetSelectedItems();
            SetText();
        } 

 

© . All rights reserved.