WPF ComboBox の ItemsSource バインドパターンと選択値の取得方法

ItemsSource に渡すデータ構造によって、DisplayMemberPath・SelectedItem・SelectedValue の設定方法が変わります。文字列リスト、オブジェクトリスト、Enum の各パターンを整理します。

概要

WPF の ComboBoxItemsSource に渡すデータ構造に応じて、表示内容の制御方法と選択値の取得方法が変わる具体的には DisplayMemberPathItemTemplateSelectedItemSelectedValueSelectedValuePath の組み合わせが、バインドするコレクションの型によって異なる。
本記事では代表的な実装パターンを整理し、それぞれの選択に関するプロパティの使い分けを示す。

前提・対象環境


問題

ComboBoxItemsSource に渡すコレクションの要素型が変わると、選択値の取得方法も変わるたとえば文字列リストを渡す場合と、ID と名称を持つオブジェクトのリストを渡す場合では、ViewModel にバインドするプロパティの型と設定が異なる。
この違いを把握していないと、選択しても値が反映されない、または初期値が表示されないといった問題が発生する。

原因・背景

ComboBox の選択関連プロパティは以下の 3 種類がある。

プロパティ 返す値 主な用途
SelectedItem ItemsSource の要素そのもの オブジェクト全体を ViewModel に渡す
SelectedValue SelectedValuePath で指定したプロパティ値 ID など特定フィールドだけを取得する
SelectedIndex 選択行のインデックス(0 始まり) 位置だけを管理する場合

文字列リストの場合、要素そのものが文字列であるため SelectedItemstring を返すオブジェクトのリストを使い SelectedValuePath を指定すると、SelectedValue には指定プロパティの値が返る。
どのプロパティをバインドするかはデータ構造に依存するため、構造ごとに設定を合わせる必要がある。

解決方法

実装に入る前に、ItemsSource の要素型に応じて選択関連プロパティを使い分ける。

この方針で選べば、ViewModel 側のプロパティ型と ComboBox の設定を対応させやすくなり、初期選択や選択変更の反映漏れを防ぎやすい。

設定の違いは、同じ選択肢を選んだときの表示に現れる。

3 つの ComboBox を並べた画面。文字列リストと DisplayMemberPath は Baker とだけ表示され、ItemTemplate を使ったものは 102 と Baker の 2 列で表示されている。
いずれも 2 件目を選択した状態。文字列リストと DisplayMemberPath は 1 つの文字列を表示するのに対し、ItemTemplate を使うと選択済みの表示にもテンプレートが適用され、複数フィールドを並べられる。

実装例

パターン A:文字列リスト

ItemsSource が ObservableCollection<string> のとき、DisplayMemberPath は不要であり、SelectedItemstring 型のプロパティをバインドするだけで機能する。

<ComboBox ItemsSource="{Binding Regions}"
          SelectedItem="{Binding SelectedRegion}" />

対応する ViewModel 側の実装は次のとおりである。

public ObservableCollection<string> Regions { get; } = new()
{
    "東北", "関東", "中部", "近畿", "九州"
};

private string? _selectedRegion;
public string? SelectedRegion
{
    get => _selectedRegion;
    set { _selectedRegion = value; OnPropertyChanged(); }
}

SelectedItemstring 以外の型(例: int)をバインドすると型の不一致でバインドエラーとなる。


パターン B:オブジェクトリスト + DisplayMemberPath + SelectedItem

ItemsSource が ObservableCollection<T> で、表示する文字列と選択して取得するオブジェクトが同じ型の場合、DisplayMemberPath で表示プロパティを指定し、SelectedItem にオブジェクト全体をバインドする。

<ComboBox ItemsSource="{Binding Departments}"
          DisplayMemberPath="Name"
          SelectedItem="{Binding SelectedDepartment}" />

上記の XAML では Department オブジェクトの Name を表示しつつ、選択結果として Department オブジェクト全体を SelectedDepartment にバインドしている。
対応するモデルと ViewModel の定義例は次のとおり。

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

public ObservableCollection<Department> Departments { get; } = new()
{
    new Department { Id = 1, Name = "営業部" },
    new Department { Id = 2, Name = "開発部" },
    new Department { Id = 3, Name = "総務部" },
};

private Department? _selectedDepartment;
public Department? SelectedDepartment
{
    get => _selectedDepartment;
    set { _selectedDepartment = value; OnPropertyChanged(); }
}

選択後は SelectedDepartment.IdSelectedDepartment.Name で任意のフィールドにアクセスできるViewModel にオブジェクト全体を保持するため、後から複数フィールドを参照しやすい。

パターン C:オブジェクトリスト + DisplayMemberPath + SelectedValuePath

表示は Name で行い、選択値として Id(数値や文字列のキー)だけを取得したい場合に使う。
SelectedValuePath に取得したいプロパティ名を指定し、SelectedValue にそのプロパティの型でバインドする。

<ComboBox ItemsSource="{Binding Departments}"
          DisplayMemberPath="Name"
          SelectedValuePath="Id"
          SelectedValue="{Binding SelectedDepartmentId}" />

対応する ViewModel 側では、選択された Id を保持するプロパティを次のように定義する。

private int _selectedDepartmentId;
public int SelectedDepartmentId
{
    get => _selectedDepartmentId;
    set { _selectedDepartmentId = value; OnPropertyChanged(); }
}

SelectedValue の型と SelectedValuePath で指定するプロパティの型が一致していないと、選択値が反映されない。
また SelectedItemSelectedValue は同時に利用できるが、一方を変更するともう一方も自動で更新される。

パターン D:ItemTemplate を使ったカスタム表示

1 行の表示に複数フィールドを含めたい場合や、アイコン付きの選択肢を実装したい場合は ItemTemplate を使う。
DisplayMemberPathItemTemplate は両方を設定できるが、表示には ItemTemplate が優先されて DisplayMemberPath は無視される。
そのため、カスタム表示が必要なときは ItemTemplate を使用し、通常は DisplayMemberPath を併用しない。

<ComboBox ItemsSource="{Binding Employees}"
          SelectedItem="{Binding SelectedEmployee}">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Id}" Width="40" Foreground="Gray"/>
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

この XAML で参照する Employee モデルの定義例は次のとおりである。

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

ItemTemplate を使う場合、ドロップダウンを閉じた状態(選択済み表示)と展開した一覧で異なるレイアウトを出したいときは、ItemTemplate の代わりに ItemContainerStyleContentTemplate を組み合わせる。


パターン E:Enum リスト

選択肢が固定の列挙体の場合、ObjectDataProvider を使って XAML から Enum 値を列挙する方法と、ViewModel でコレクションを用意する方法がある。
保守性を考えると ViewModel でコレクションを生成する方法が安全である。
ViewModel 側で Enum.GetValues を使って選択肢を生成する。

public enum Priority { , ,  }

public IEnumerable<Priority> Priorities { get; }
    = (Priority[])Enum.GetValues(typeof(Priority));

private Priority _selectedPriority = Priority.;
public Priority SelectedPriority
{
    get => _selectedPriority;
    set { _selectedPriority = value; OnPropertyChanged(); }
}

この ViewModel に対して、XAML 側では ItemsSourceSelectedItem を次のようにバインドする。

<ComboBox ItemsSource="{Binding Priorities}"
          SelectedItem="{Binding SelectedPriority}" />

SelectedItem の型は Priority(Enum 型)になるSelectedValueSelectedValuePath を使って Enum の数値(基底値)だけを取得することも可能だが、明示的に (int)SelectedPriority でキャストした方が意図が明確である。

注意点


代替案・比較

パターン ItemsSource の型 選択値の取得 適するケース
A: 文字列リスト ObservableCollection<string> SelectedItemstring 選択肢が単純なラベルのみ
B: オブジェクト + SelectedItem ObservableCollection<T> SelectedItemT 選択後に複数フィールドを参照する
C: オブジェクト + SelectedValuePath ObservableCollection<T> SelectedValue(指定プロパティ型) ID など特定フィールドだけ必要
D: ItemTemplate ObservableCollection<T> SelectedItemT 複数フィールドを 1 行に表示する
E: Enum IEnumerable<TEnum> SelectedItemTEnum 固定の列挙値から選択する

まとめ

ComboBox の実装パターンは ItemsSource に渡す型によって決まる。

初期値が反映されない問題の多くは、参照比較の不一致か ItemsSource のセット順序に起因する。
SelectedValuePath を使うか、同一インスタンスを参照するよう設計することで回避できる。