web-dev-qa-db-ja.com

WPF ComboBoxをカスタムリストにバインドする

SelectedItem/SelectedValueを更新しないようなComboBoxがあります。

ComboBox ItemsSourceは、一連のRAS電話帳エントリをCollectionViewとして一覧表示するViewModelクラスのプロパティにバインドされています。それからSelectedItemSelectedValueの両方をViewModelの別のプロパティにバインドしました。 databindingによって設定された値をデバッグするためにsaveコマンドにMessageBoxを追加しましたが、SelectedItem/SelectedValueバインディングは設定されていません。

ViewModelクラスは次のようになります。

public ConnectionViewModel
{
    private readonly CollectionView _phonebookEntries;
    private string _phonebookeEntry;

    public CollectionView PhonebookEntries
    {
        get { return _phonebookEntries; }
    }

    public string PhonebookEntry
    {
        get { return _phonebookEntry; }
        set
        {
            if (_phonebookEntry == value) return;
            _phonebookEntry = value;
            OnPropertyChanged("PhonebookEntry");
        }
    }
}

_phonebookEntriesコレクションは、ビジネス・オブジェクトからコンストラクター内で初期化されています。 ComboBox XAMLは次のようになります。

<ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
    DisplayMemberPath="Name"
    SelectedValuePath="Name"
    SelectedValue="{Binding Path=PhonebookEntry}" />

これは、VPN接続を確立するときにRASに渡す必要がある値なので、オブジェクトの他のプロパティではなく、ComboBoxに表示される実際の文字列値にのみ関心があります。したがってDisplayMemberPathSelectedValuePathは、どちらもConnectionViewModelのNameプロパティです。 ComboBoxは、DataContextがViewModelインスタンスに設定されているWindow上のDataTemplateに適用されるItemsControl内にあります。

ComboBoxはアイテムのリストを正しく表示するので、UIで問題なくアイテムを選択できます。ただし、コマンドからメッセージボックスを表示しても、PhonebookEntryプロパティには初期値があり、ComboBoxから選択した値はありません。他のTextBoxインスタンスは正常に更新され、MessageBoxに表示されています。

ComboBoxのデータバインディングに欠けているものは何ですか?私はたくさんの検索をしましたが、私が間違っていることを見つけることはできません。


これは私が見ている振る舞いですが、それは私の特定の状況では何らかの理由で機能していません。

ConnectionViewModelのCollectionViewを持つMainWindowViewModelがあります。コードビハインドのMainWindowView.xamlファイルで、DataContextをMainWindowViewModelに設定します。 MainWindowView.xamlには、ConnectionViewModelのコレクションにバインドされたItemsControlがあります。私は他のいくつかのTextBoxと同様にComboBoxを保持するDataTemplateを持っています。 TextBoxは、Text="{Binding Path=ConnectionName}"を使用してConnectionViewModelのプロパティに直接バインドされています。

public class ConnectionViewModel : ViewModelBase
{
    public string Name { get; set; }
    public string Password { get; set; }
}

public class MainWindowViewModel : ViewModelBase
{
    // List<ConnectionViewModel>...
    public CollectionView Connections { get; set; }
}

XAML分離コード

public partial class Window1
{
    public Window1()
    {
        InitializeComponent();
        DataContext = new MainWindowViewModel();
    }
}

次にXAML:

<DataTemplate x:Key="listTemplate">
    <Grid>
        <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
            DisplayMemberPath="Name"
            SelectedValuePath="Name"
            SelectedValue="{Binding Path=PhonebookEntry}" />
        <TextBox Text="{Binding Path=Password}" />
    </Grid>
</DataTemplate>

<ItemsControl ItemsSource="{Binding Path=Connections}"
    ItemTemplate="{StaticResource listTemplate}" />

TextBoxはすべて正しくバインドされ、データはそれらとViewModelの間を問題なく移動します。機能していないのはComboBoxだけです。

PhonebookEntryクラスに関するあなたの仮定は正しいです。

私の仮定は、私のDataTemplateが使用するDataContextは自動的にバインディング階層を通して設定されるので、ItemsControlの各項目に明示的に設定する必要はないということです。それは私にとって少しばかげたように思えるでしょう。


上記の例に基づいて、問題を実証するテスト実装です。

XAML:

<Window x:Class="WpfApplication7.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <DataTemplate x:Key="itemTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBox Text="{Binding Path=Name}" Width="50" />
                <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
                    DisplayMemberPath="Name"
                    SelectedValuePath="Name"
                    SelectedValue="{Binding Path=PhonebookEntry}"
                    Width="200"/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ItemsControl ItemsSource="{Binding Path=Connections}"
            ItemTemplate="{StaticResource itemTemplate}" />
    </Grid>
</Window>

分離コード:

namespace WpfApplication7
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel();
        }
    }

    public class PhoneBookEntry
    {
        public string Name { get; set; }
        public PhoneBookEntry(string name)
        {
            Name = name;
        }
    }

    public class ConnectionViewModel : INotifyPropertyChanged
    {

        private string _name;

        public ConnectionViewModel(string name)
        {
            _name = name;
            IList<PhoneBookEntry> list = new List<PhoneBookEntry>
                                             {
                                                 new PhoneBookEntry("test"),
                                                 new PhoneBookEntry("test2")
                                             };
            _phonebookEntries = new CollectionView(list);
        }
        private readonly CollectionView _phonebookEntries;
        private string _phonebookEntry;

        public CollectionView PhonebookEntries
        {
            get { return _phonebookEntries; }
        }

        public string PhonebookEntry
        {
            get { return _phonebookEntry; }
            set
            {
                if (_phonebookEntry == value) return;
                _phonebookEntry = value;
                OnPropertyChanged("PhonebookEntry");
            }
        }

        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value) return;
                _name = value;
                OnPropertyChanged("Name");
            }
        }
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class MainWindowViewModel
    {
        private readonly CollectionView _connections;

        public MainWindowViewModel()
        {
            IList<ConnectionViewModel> connections = new List<ConnectionViewModel>
                                                          {
                                                              new ConnectionViewModel("First"),
                                                              new ConnectionViewModel("Second"),
                                                              new ConnectionViewModel("Third")
                                                          };
            _connections = new CollectionView(connections);
        }

        public CollectionView Connections
        {
            get { return _connections; }
        }
    }
}

あなたがその例を実行するならば、あなたは私が話している行動を得るでしょう。 TextBoxは、編集時にそのバインディングを正しく更新しますが、ComboBoxは更新しません。私がやったことが本当に唯一のことだと考えるのが非常に混乱するのは、親ViewModelを紹介することです。

私は現在、DataContextの子にバインドされている項目はその子をそのDataContextとして持っているという印象の下で作業しています。これを片方向に片付ける文書が見つかりません。

すなわち、

ウィンドウ - > DataContext = MainWindowViewModel
..アイテム - > DataContext.PhonebookEntriesにバインド
.... Item - > DataContext = PhonebookEntry(暗黙的に関連付けられています)

それが私の仮定をもっとよく説明するかどうか私は知りません(?)。


私の仮定を確認するために、TextBoxのバインディングを次のように変更します。

<TextBox Text="{Binding Mode=OneWay}" Width="50" />

そして、これはTextBoxバインディングルート(私はDataContextと比較しています)がConnectionViewModelインスタンスであることを示します。

169
Geoff Bennett

DisplayMemberPathとSelectedValuePathを "Name"に設定したので、パブリックプロパティNameを持つクラスPhoneBookEntryがあるとします。

あなたはあなたのConnectionViewModelオブジェクトにDataContextを設定しましたか?

私はあなたのコードをコピーし、いくつかの小さな修正を加えました、そしてそれはうまくいくようです。 viewmodelsのPhoneBookEntyプロパティを設定してコンボボックス内の選択した項目を変更することができます。また、コンボボックス内の選択した項目を変更して、ビューモデルのPhoneBookEntryプロパティを正しく設定できます。

これが私のXAMLコンテンツです。

<Window x:Class="WpfApplication6.Window1"
        xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
<Grid>
    <StackPanel>
        <Button Click="Button_Click">asdf</Button>
        <ComboBox ItemsSource="{Binding Path=PhonebookEntries}"
                  DisplayMemberPath="Name"
                  SelectedValuePath="Name"
                  SelectedValue="{Binding Path=PhonebookEntry}" />
    </StackPanel>
</Grid>
</Window>

そして、これが私のコードビハインドです。

namespace WpfApplication6
{

    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            ConnectionViewModel vm = new ConnectionViewModel();
            DataContext = vm;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            ((ConnectionViewModel)DataContext).PhonebookEntry = "test";
        }
    }

    public class PhoneBookEntry
    {
        public string Name { get; set; }

        public PhoneBookEntry(string name)
        {
            Name = name;
        }

        public override string ToString()
        {
            return Name;
        }
    }

    public class ConnectionViewModel : INotifyPropertyChanged
    {
        public ConnectionViewModel()
        {
            IList<PhoneBookEntry> list = new List<PhoneBookEntry>();
            list.Add(new PhoneBookEntry("test"));
            list.Add(new PhoneBookEntry("test2"));
            _phonebookEntries = new CollectionView(list);
        }

        private readonly CollectionView _phonebookEntries;
        private string _phonebookEntry;

        public CollectionView PhonebookEntries
        {
            get { return _phonebookEntries; }
        }

        public string PhonebookEntry
        {
            get { return _phonebookEntry; }
            set
            {
                if (_phonebookEntry == value) return;
                _phonebookEntry = value;
                OnPropertyChanged("PhonebookEntry");
            }
        }

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

編集:Geoffsの2番目の例はうまくいかないようですが、これは私には少し奇妙に思えます。 IをConnectionViewModelのPhonebookEntriesプロパティをReadOnlyCollection型に変更すると、コンボボックスのSelectedValueプロパティのTwoWayバインディングは正常に機能します。

CollectionViewに問題があるのでしょうか。出力コンソールに警告が表示されました。

System.Windows.Data警告:50:CollectionViewを直接使用することは完全にはサポートされていません。基本的な機能は機能しますが、非効率的ですが、高度な機能は既知のバグに遭遇する可能性があります。これらの問題を回避するために派生クラスを使用することを検討してください。

Edit2(.NET 4.5):DropDownListの内容は、DisplayMemberPathではなくToString()に基づくことができます。一方、DisplayMemberPathは、選択され表示されている項目に対してのみメンバーを指定します。

175
Kjetil Watnedal

データをComboBoxにバインドする

List<ComboData> ListData = new List<ComboData>();
ListData.Add(new ComboData { Id = "1", Value = "One" });
ListData.Add(new ComboData { Id = "2", Value = "Two" });
ListData.Add(new ComboData { Id = "3", Value = "Three" });
ListData.Add(new ComboData { Id = "4", Value = "Four" });
ListData.Add(new ComboData { Id = "5", Value = "Five" });

cbotest.ItemsSource = ListData;
cbotest.DisplayMemberPath = "Value";
cbotest.SelectedValuePath = "Id";

cbotest.SelectedValue = "2";

ComboDataは次のようになります。

public class ComboData
{ 
  public int Id { get; set; } 
  public string Value { get; set; } 
}
71
Roy

私は最初は全く同じ問題と思われるものを持っていましたが、それはNHibernate/WPFの互換性の問題が原因であることが判明しました。この問題は、WPFがオブジェクトの等価性をチェックする方法によって引き起こされました。 SelectedValueプロパティとSelectedValuePathプロパティのオブジェクトIDプロパティを使用して、機能するようになりました。

<ComboBox Name="CategoryList"
          DisplayMemberPath="CategoryName"
          SelectedItem="{Binding Path=CategoryParent}"
          SelectedValue="{Binding Path=CategoryParent.ID}"
          SelectedValuePath="ID">

Chesterによるブログ投稿を参照してください。WPFコンボボックス - SelectedItem、SelectedValue、およびSelectedValuePath with NHibernate詳細については。

22
CyberMonk

SelectedItemが更新されないという同様の問題がありました。

私の問題は、選択された項目がリストに含まれている項目と同じインスタンスではないということでした。そのため、MyCustomObjectのEquals()メソッドをオーバーライドし、これら2つのインスタンスのIDを比較して、それが同じオブジェクトであることをComboBoxに伝えるだけで済みました。

public override bool Equals(object obj)
{
    return this.Id == (obj as MyCustomObject).Id;
}
1
phifi