C#でコレクションの要素の変更をバインド先のコントロールに反映する方法

C#でコレクションをListViewなどのコントロールにバインディングしているときに、コレクションの要素の変更を自動的にバインド先のコントロールに反映する方法です。

目次

  1. ObservableCollectionクラスは要素の変更を通知しない
    1. 具体例
  2. 変更を通知する仕組みを作る
    1. 実施例

ObservableCollectionクラスは要素の変更を通知しない

ListViewなどにバインドするコレクションを定義する際には、ObservableCollectionを使うと思います。

ObservableCollectionは、コレクションのアイテムが追加または削除されたとき、あるいはリスト全体が更新されたときに、通知を行います。

ただし、そのコレクションの要素のプロパティが変更された場合には、通知は行いません。

具体例

実際に通知が行われない例を見てみましょう。

ListViewとTextBoxから成るアプリで、リストの選択したアイテムをTextBoxに表示し、TextBoxを変更するとそのようにデータを書き換えるアプリです。

<Window x:Class="listview_test.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:listview_test"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="400" Loaded="Window_Loaded">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="150" />
        </Grid.ColumnDefinitions>
        <ListView Name="ListViewMain" Grid.Column="0" ItemsSource="{Binding}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10" SelectionChanged="ListViewMain_SelectionChanged" >
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding Path=Moji}">
                        <GridViewColumnHeader Content="文字" />
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
        <StackPanel Grid.Column="1">
            <TextBox Name="TextBox1" Margin="10,40,10,10" TextChanged="TextBox1_TextChanged" />
        </StackPanel>
    </Grid>
</Window>

GridViewColumnにバインディングのPathを指定します。

WindowにLoadedイベントを、ListViewにSelectionChangedイベントを、TextBoxにTextChangedイベントを設定します。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;

namespace listview_test
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        ObservableCollection<Hoge> Piyo = new ObservableCollection<Hoge>();

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            Piyo.Add(new Hoge() { Moji = "いろは" });
            Piyo.Add(new Hoge() { Moji = "にほへと" });
            Piyo.Add(new Hoge() { Moji = "ちりぬるを" });
            ListViewMain.DataContext = Piyo;
        }

        private void ListViewMain_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (ListViewMain.SelectedItem == null) return;

            // 選択したアイテムをTextBoxに表示する
            Hoge item = (Hoge)ListViewMain.SelectedItem;
            TextBox1.Text = item.Moji;
        }

        private void TextBox1_TextChanged(object sender, TextChangedEventArgs e)
        {
            int i = ListViewMain.SelectedIndex;
            if (i < 0) return;

            // TextBoxの変更内容をデータに反映する
            Piyo[i].Moji = TextBox1.Text;
        }
    }

    public class Hoge
    {
        public string Moji { set; get; }
    }

Windowをロードすると、Piyoオブジェクトにアイテムを3つ追加してListViewにバインディングします。

ListViewのSelectionChangedイベントが起きると、ListViewで選択されたアイテムを取り出して、そのMojiプロパティの内容をTextBoxのTextプロパティにセットします。

TextBoxのTextChangedイベントが起きると、コレクションの内容をTextプロパティの内容に書き換えます。

これを実行すると、このようになります。

反映されない例

TextBoxを書き換えたのに、ListViewには反映されていないですね。

実際のところ、データの方は変更されているのですが、ListViewの表示が変更されないという状態になっています。

コレクションの要素のプロパティが変更されたのに、その変更がバインド先のコントロールに反映されていないという状態です。

変更を通知する仕組みを作る

ObservableCollectionだけではプロパティの変更を通知してくれないので、コレクションを作成するクラスの方に通知イベントを定義します。

イベントを定義したクラスのコードは下記のようになります。

public class Hoge : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(String propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    private string _Moji = string.Empty;
    public string Moji
    {
        get { return this._Moji; }
        set
        {
            if (value != this._Moji)
            {
                this._Moji = value;
                NotifyPropertyChanged();
            }
        }
    }
}

まず、INotifyPropertyChangedインターフェイスを継承します。

そして、PropertyChangedイベントを定義します。

さらに、NotifyPropertyChangedというprivateメソッドを定義します。このメソッドはPropertyChangedイベントを発行するメソッドです。

privateなプロパティを宣言します。(ここでは_Mojiです。)これは、プロパティの変更前の状態を保持しておくためのものです。

プロパティのgetterは、保持しているprivateプロパティの値を返します。setterに値がセットされると、変更前の値と比較して、セットされた値が変更前の値と異なる場合はNotifyPropertyChangedメソッドを呼び出します。

こうして、このクラスのプロパティのsetterに異なる値がセットされるとPropertyChangedが発行されるわけです。

実施例

このコードで実行してみましょう。

XAMLは変更ないので省略します。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace listview_test
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        ObservableCollection<Hoge> Piyo = new ObservableCollection<Hoge>();

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            Piyo.Add(new Hoge() { Moji = "いろは" });
            Piyo.Add(new Hoge() { Moji = "にほへと" });
            Piyo.Add(new Hoge() { Moji = "ちりぬるを" });
            ListViewMain.DataContext = Piyo;
        }

        private void ListViewMain_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (ListViewMain.SelectedItem == null) return;

            // 選択したアイテムをTextBoxに表示する
            Hoge item = (Hoge)ListViewMain.SelectedItem;
            TextBox1.Text = item.Moji;
        }

        private void TextBox1_TextChanged(object sender, TextChangedEventArgs e)
        {
            int i = ListViewMain.SelectedIndex;
            if (i < 0) return;

            // TextBoxの変更内容をデータに反映する
            Piyo[i].Moji = TextBox1.Text;
        }
    }

    public class Hoge : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        private string _Moji = string.Empty;
        public string Moji
        {
            get { return this._Moji; }
            set
            {
                if (value != this._Moji)
                {
                    this._Moji = value;
                    NotifyPropertyChanged();
                }
            }
        }
    }
}

usingにSystem.ComponentModelが必要なので注意してください。

クラスの定義が変わっただけです。

実行するとこうなります。

反映する例

TextBoxの変更内容がListViewに反映されます。

公開日

広告