[WPF] ObserbleCollection で編集(更新)を変更通知で反映したい

[WPF] ObserbleCollection で編集(更新)を変更通知で反映したい

リスト(コレクション)データをバインドして変更通知したい

WPF でリスト(コレクション)データをバインドして、変更通知を行う方法をまとめます。特にバインドされたデータの値を書き換える場合や、バインドされたデータのソートは、単純に ObservableCollection を使用するだけではうまくいきません。

この記事では例として以下のような操作について実装方法をまとめます。

  • データの追加
  • データの削除
  • データの編集(更新)
  • データのソート

なおデータバインドには Prism を使用します。

実装

コレクションデータをバインドしてデータの変更を通知したい場合 List ではなく ObservableCollection を使います。

コードの全体は以下のようになります。

  • MainWindow.xaml
    <StackPanel Orientation="Vertical">
      <Button Command="{Binding AddCommand}">追加</Button>
      <Button Command="{Binding RemoveCommand}">削除</Button>
      <Button Command="{Binding EditCommand}">編集</Button>
      <Button Command="{Binding SortCommand}">並び替え</Button>
      <ListView ItemsSource="{Binding MyDataList}">
          <ListView.View>
              <GridView>
                  <GridView.Columns>
                      <GridViewColumn>
                          <GridViewColumnHeader>更新日時</GridViewColumnHeader>
                          <GridViewColumn.CellTemplate>
                              <DataTemplate>
                                  <TextBlock Text="{Binding UpdatedAt}"></TextBlock>
                              </DataTemplate>
                          </GridViewColumn.CellTemplate>
                      </GridViewColumn>
                  </GridView.Columns>
              </GridView>
          </ListView.View>
      </ListView>
    </StackPanel>
    
  • MainWindow.xaml.cs
    “`cs
    public partial class MainWindow : Window
    {
    public MainWindow()
    {

      InitializeComponent();
      var vm = new MyViewModel();
      vm.MyDataList = new ObservableCollection<MyModel>();
      this.DataContext = vm;
    

    }
    }

class MyViewModel: BindableBase
{
// データリスト
public ObservableCollection MyDataList { get; set; }

// データの追加
private DelegateCommand _addCommand;
public DelegateCommand AddCommand => _addCommand = _addCommand ?? new DelegateCommand(this.Add);
public void Add()
{
    this.MyDataList.Add(new MyModel());
}

// データの削除
private DelegateCommand _removeCommand;
public DelegateCommand RemoveCommand => _removeCommand = _removeCommand ?? new DelegateCommand(this.Remove);
public void Remove()
{
    if (this.MyDataList.Count == 0) return;
    var rnd = new Random();
    var index = rnd.Next(0, this.MyDataList.Count - 1);
    this.MyDataList.RemoveAt(index);
}

// データの編集(更新)
private DelegateCommand _editCommand;
public DelegateCommand EditCommand => _editCommand = _editCommand ?? new DelegateCommand(this.Edit);
public void Edit()
{
    if (this.MyDataList.Count == 0) return;
    var rnd = new Random();
    var index = rnd.Next(0, this.MyDataList.Count - 1);
    var data = this.MyDataList[index];
    data.UpdatedAt = DateTime.Now;
}

// データのソート
private DelegateCommand _sortCommand;
public DelegateCommand SortCommand => _sortCommand = _sortCommand ?? new DelegateCommand(this.Sort);
public void Sort()
{
    var sorted = this.MyDataList.OrderBy(x => x.UpdatedAt).ToList();
    this.MyDataList.Clear();
    foreach (var item in sorted) this.MyDataList.Add(item);
}

}

public class MyModel : BindableBase
{
private DateTime _updatedAt;
public DateTime UpdatedAt { get => _updatedAt; set => SetProperty(ref _updatedAt, value); }

public MyModel()
{
    this.UpdatedAt = DateTime.Now;
}

}


`ListView` に日時だけを表示するような View です。そこに `ObservableCollection` をバインドしています。

3つのボタンを用意し、追加ボタンでデータの追加、削除ボタンでランダムな1件のデータの削除、編集ボタンでランダムな1件のデータの日時を書き換え、並び替えボタンでデータのソートを行います。


### データの追加と削除

`List` をバインドするとバインドされた時点でのデータから書き換えることが基本的にできないですが、これを `ObservableCollection` に変更するだけでデータの追加や削除ができます。

追加には `Add()` や `Insert()` を、削除には `Remove()` や `RemoveAt()` 使用します。

`ObservableCollection<T>` 型は `Collection<T>` 型を継承しているのでコレクションが持つだいたいのインターフェースをそのまま利用できます。

### データの編集

`ObservableCollection` はそれ自体がすでに変更通知機能を実装しているため、データが追加されたり削除されたりした場合に、それをビュー側に通知して表示内容の更新が行われます。

この時監視しているのがおそらく個々のデータのインスタンスが同じかどうかなので、単純に以下のように特定のインスタンスの内部の値だけを更新してもこれを通知してくれません。

```cs
// うまくいかない例

// ランダムに1件のデータ(インスタンス)を取得
if (this.MyDataList.Count == 0) return;
var rnd = new Random();
var index = rnd.Next(0, this.MyDataList.Count - 1);
var data = this.MyDataList[index];

// インスタンスの内部データを更新
data.UpdatedAt = DateTime.Now;

// ※バインドされているデータの型
// public class MyModel
// {
//     public DateTime UpdatedAt { get; set; }
//     public MyModel()
//     {
//         this.UpdatedAt = DateTime.Now;
//     }
// }

もしこれをうまく通知したい場合、ObservableCollection<T> 型の T 型についても変更通知を実装する必要があります。Prism を使用しているので BindableBase を継承します。

この例では以下のようなクラスを作成するとうまくいきます。

public class MyModel : BindableBase
{
   private DateTime _updatedAt;
   public DateTime UpdatedAt { get => _updatedAt; set => SetProperty(ref _updatedAt, value); }

   public MyModel()
   {
       this.UpdatedAt = DateTime.Now;
   }
}

こうすることで内部データの変更通知がビューまで反映されて画面描画が更新されます。

インスタンスを差し替える

もし T 型が変更通知を実装しない場合でも、ObservableCollection<T> のデータをインスタンスごと差し替えるとうまくいきます。

例えば以下のようなコードになります。

if (this.MyDataList.Count == 0) return;
var rnd = new Random();
var index = rnd.Next(0, this.MyDataList.Count - 1);

// インスタンスごと差し替える
this.MyDataList[index] = new MyModel();

したがって ObservableCollection<int> など値型のコレクションであれば値が常に新しく生成されるので変更通知もうまくいきます。

データのソート

ObservableCollection<T>Linq を使ってソートすることができますが、Linq のソートは非破壊的(ソート前のコレクションを更新しない)なので以下のようにしてもうまくいきません。

// うまくいかないソート
this.MyDataList.OrderBy(x => x.UpdatedAt);

そこで次のように新しいソート後のデータで ObservableCollection を新しく生成することを考えますが、これもうまくいきません。バインドされているデータ(ObservableCollection)のインスタンスごと別物に変わっているため、バインドされた状態が機能しなくなります。

// うまくいかないソート
this.MyDataList = new ObservableCollection<MyModel>(this.MyDataList.OrderBy(x => x.UpdatedAt));

ソートを実現するために ObservableCollection の中身をからにして、そのあとソート後のデータを詰めなおすという方法を採るとうまくいきます。

// うまくいくソート
var sorted = this.MyDataList.OrderBy(x => x.UpdatedAt).ToList();
this.MyDataList.Clear();
foreach (var item in sorted) this.MyDataList.Add(item);

若干冗長なコードな気がしますが、これでうまくいったのでよしとします。

以上。

.NET Frameworkカテゴリの最新記事