【.NET】コンボボックスで複数選択する方法

# はじめに
これは、[Visual Basic Advent Calendar 2020](https://qiita.com/advent-calendar/2020/vb)の18日目の記事となります。

同僚から複数選択のコンボボックスってないですか？って聞かれて、標準のコンボボックスは複数選択できないんだったっけと調べたんですが、出来ませんでした。
検索すれば、どこかにあるだろうって「c# コンボボックス 複数選択」で検索するも見当たらない。キーワードを英語に変更「c# custom control combobox multiselect」するなどして、ようやく下記サイトから辿ることができた。
[how to do Multi select dropdown list/Combobox C# windows application](https://www.c-sharpcorner.com/forums/how-to-do-multi-select-dropdown-listcombobox-c-sharp-windows-application)

【2020/12/19追記】
18日の記事に合わせるために慌てて書いたので、翌日に再度落ち着いて調べ直しました。
検索キーワードとしては、「c# checkedlistbox DropDown」で調べるともう少し情報が得られました。
WinFormは、③と④を追加、Wpfは全般見直し。

# WinForm
①と②は C# でカスタムコントロールを作成しています。
どちらも「[CodeProject](https://www.codeproject.com/)」に登録されているので、ダウンロードするにはアカウントが必要になります。
③と④は、VB.NET でカスタムコントロールを作成しています。

  * ① [CheckBox ComboBox Extending the ComboBox Class and Its Items]
(https://www.codeproject.com/Articles/21085/CheckBox-ComboBox-Extending-the-ComboBox-Class-and)
  * ② [A ComboBox with a CheckedListBox as a Dropdown](https://www.codeproject.com/Articles/31105/A-ComboBox-with-a-CheckedListBox-as-a-Dropdown)
  * ③ [Make DropDown CheckedListBox in VB.net](https://www.c-sharpcorner.com/UploadFile/hirendra_singh/make-dropdown-checkedlistbox-in-VB-Net/)
  * ④ [VB.NET 版 CheckedComboBox - 周回遅れのブルース](https://hilapon.hatenadiary.org/entry/20100406/1270552723)

お薦めは①の方になります。「CheckBoxComboBox.dll」が作成されるので、Visual Basicでも参照に追加することで使用できます。
作成時期が古いのでターゲットフレームワークは「.NET Framework 3.5」になっていますが、そこは最新の「.NET Framework 4.8」に変更するなりしてください。

```vb
Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        cmbManual.Items.Add("Item 1")
        cmbManual.Items.Add("Item 2")
        cmbManual.Items.Add("Item 3")
        cmbManual.Items.Add("Item 4")
        cmbManual.Items.Add("Item 5")
        cmbManual.Items.Add("Item 6")
        cmbManual.Items.Add("Item 7")
        cmbManual.Items.Add("Item 8")

        cmbManual.CheckBoxItems(1).Checked = True
    End Sub
End Class
```
![MultiSelectComboBox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/ea9709cc-fdf7-b4b9-2e65-f4b56efa4af5.png)

# Wpf
* ① [Multi Select ComboBox in WPF](https://www.c-sharpcorner.com/UploadFile/1a81c5/multi-select-combobox-in-wpf/)
* ② [Multi Select ComboBox in WPF - CodeProject](https://www.codeproject.com/Articles/563862/Multi-Select-ComboBox-in-WPF)
* ③ [C# WPF で複数のタグを選択できるコンボボックスを作る](https://mseeeen.msen.jp/wpf-multi-tag-combo-box/)
* ④ [WPF CheckListBox/RadioListBox + CheckComboBox/RadioComboBox](https://www.codeproject.com/Articles/30142/WPF-CheckListBox-RadioListBox-CheckComboBox-RadioC)

お薦めは②の方になります。①と②は作者は同じなんですが、②の方が記事の更新が新しくコンボボックスの右側に「▼」が追加されています。
③は、リストではないですが複数の値を選択できるということ挙げました。

![MultiSelectComboBoxWpf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/cc10ca06-edd0-90b1-e48f-0679ab3b22f8.png)

①のコメント欄にあった、dictionary型よりList型が一般的とのソースコードを以下に書き直しました。

>This is Great but the code as is is not exploitable.In the normal life we use List not dictionary and it should work properly on MVVM.The solution is in MultiSelectComboBox.xaml.cs

```csharp:MultiSelectComboBox.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace MultiSelectComboBox
{
    /// <summary>
    /// Interaction logic for MultiSelectComboBox.xaml
    /// </summary>
    public partial class MultiSelectComboBox : UserControl 
    {
        private readonly ObservableCollection<Node> _nodeList;
        public MultiSelectComboBox()
        {
            InitializeComponent();
            _nodeList = new ObservableCollection<Node>();
        } 
        
        #region Dependency Properties
        
        public static readonly DependencyProperty ItemsSourceProperty = 
            DependencyProperty.Register("ItemsSource", typeof(IList), typeof(MultiSelectComboBox),
        new FrameworkPropertyMetadata(null, OnItemsSourceChanged));
        
        public static readonly DependencyProperty SelectedItemsProperty = 
         DependencyProperty.Register("SelectedItems", typeof(IList), typeof(MultiSelectComboBox), 
        new FrameworkPropertyMetadata(null, OnSelectedItemsChanged));
        
        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 IList ItemsSource 
        { 
            get { return (IList)GetValue(ItemsSourceProperty); } 
            set 
            { 
                SetValue(ItemsSourceProperty, value);
            } 
        }
        
        public IList SelectedItems 
        {
            get { return (IList)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); }
        }
        #endregion

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

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

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

            if (clickedBox.Content != null && clickedBox.Content.ToString() == "All")
            {
                if (clickedBox.IsChecked.HasValue && clickedBox.IsChecked.Value)
                {
                    foreach (var node in _nodeList)
                        node.IsSelected = true;
                }
                else 
                {
                    foreach (var node in _nodeList)
                        node.IsSelected = false;
                }
            }
            else
            {
                var selectedCount = _nodeList.Count(s => s.IsSelected && s.Title != "All");
                var node = _nodeList.FirstOrDefault(i => i.Title == "All");
                if (node != null)
                    node.IsSelected = selectedCount == _nodeList.Count - 1;
            }
            SetSelectedItems();
            SetText();
        }
        #endregion

        #region Methods
        private void SelectNodes()
        {
            if (SelectedItems == null)
                return;

            foreach (var item in SelectedItems)
            {
                var node = _nodeList.FirstOrDefault(i => i.Title == item.ToString());
                if (node != null)
                    node.IsSelected = true;
            }
        }
        private void SetSelectedItems() 
        {
            SelectedItems.Clear();
            foreach (var node in _nodeList)
            {
                if (!node.IsSelected || node.Title == "All")
                    continue;

                if (ItemsSource.Count <= 0)
                    continue;
                
                var source = ItemsSource.Cast<object>().ToList();
                SelectedItems.Add(source.FirstOrDefault(i => i.ToString() == node.Title));
            }
        }

        private void DisplayInControl() 
        {
            _nodeList.Clear();
            if (ItemsSource.Count > 0)
                _nodeList.Add(new Node("All"));
            foreach (var item in ItemsSource)
            {
                var node = new Node(item.ToString());
                _nodeList.Add(node);
            }       
            MultiSelectCombo.ItemsSource = _nodeList;
        }

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


        #endregion
    }
    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)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
```

# 最後に
思ったより日本語で検索しても見つからないもんですね。見つかりやすいタイトルにしてみました。