1. ホーム
  2. c#

[解決済み】WPFコンボボックスのカスタムリストへのバインド

2022-04-16 19:23:40

質問

コンボボックスがあるのですが、SelectedItem/SelectedValueが更新されないようです。

コンボボックスのItemsSourceは、CollectionViewとしてRAS電話帳のエントリーの束をリストしているViewModelクラスのプロパティにバインドされています。そして、私は(別々の時に)両方のコンボボックスのItemsourceをバインドしました。 SelectedItem または SelectedValue をViewModelの別のプロパティに追加しました。データバインディングによって設定された値をデバッグするために、保存コマンドに 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 コレクションをコンストラクターで初期化しています。コンボボックスのXAMLは以下のような感じです。

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

私は、ComboBox に表示される実際の文字列値にのみ興味があり、オブジェクトの他のプロパティには興味がありません。これは、VPN 接続を行うときに RAS に渡す必要がある値だからです。 DisplayMemberPathSelectedValuePath はいずれもConnectionViewModelのNameプロパティです。コンボボックスは DataTemplate に適用されます。 ItemsControl DataContext が ViewModel インスタンスに設定された Window 上で使用されます。

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

コンボボックスのデータバインディングで何が足りないのでしょうか?いろいろと検索してみたのですが、間違っているところが見つからないようです。


これは私が見ている動作ですが、私の特定のコンテキストでは何らかの理由で動作していません。

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

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}" />

テキストボックスはすべて正しくバインドされ、データはテキストボックスと ViewModel 間で問題なく移動します。機能していないのはコンボボックスだけです。

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; }
        }
    }
}

この例を実行すると、私が言っているような動作になるはずです。テキストボックスは編集するとバインディングが正常に更新されますが、コンボボックスは更新されません。親ViewModelを導入しただけなので、非常にわかりにくいです。

DataContextの子にバインドされたアイテムは、その子をDataContextとして持っているという印象で、現在、苦労しています。この点を明確にした文書が見当たりません。

すなわち

ウィンドウ -> DataContext = MainWindowViewModel

..Items -> DataContext.PhonebookEntriesにバインドされます。

...Item -> DataContext = PhonebookEntry (暗黙のうちに関連づけられる)

私の思い込みを少しでも説明できているかは分かりませんが(?)


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

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

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

解決するには?

DisplayMemberPathとSelectedValuePathを"Name"に設定しているので、PhoneBookEntryクラスでパブリックプロパティNameがあると推測されます。

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

あなたのコードをコピーして少し修正したところ、問題なく動作しているようです。 ビューモデルの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;
    }
}

編集する Geoffの2番目の例はうまくいかないようで、私には少し奇妙に思えます。もし私が ConnectionViewModel の PhonebookEntries プロパティを ReadOnlyCollection 型に変更します。 コンボボックスのSelectedValueプロパティのTwoWayバインディングは問題なく動作します。

もしかして、CollectionViewに問題があるのでは?出力コンソールに警告が表示されていることに気づきました。

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

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