🍐

CollectionViewの要素は、部分的に更新できないものなのか?

2023/09/10に公開

はじめに

数年前からXamarinを使い始めまして、1つのプロジェクトで3OS向けにビルドできてしまう有用性は、十分体感してきたのですが、あわせて、何か不安定で商用リリースには耐えられないなぁという感触も得ていました。原因の一つは筆者がCollectionViewを使いこなせていないことにあり、XAMLのバインディングアーキテクチャを正しく理解していないことにあるのだろうとは思っていたのですが、差し迫った案件もなく、放置プレーを続けておりました。
そうこうしているうちに、Microsoftがせっかく買収したXamarinをディスコンにするとのこと.NET MAUIに移行せよというリリースを目にし、Windows専用ならWinFormで十分と思っている筆者も、3OSで動くならということで、この機会に.NET MAUIを使って、XAMLとデータバインディングの世界に迷い込んでみました。

CollectionViewで実現したいこと

XAMLの有用性は、スライダを動かしたらリアルタイムに値が変化する等、UI同士の結びつきをいちいちコードで記述しなくて済むという点。それはわかっているのですが、筆者が携わっているアプリは、だいたいHTTPやデータベースから取得したデータで、UIを再描画するタイプのアプリなので、公式をはじめとするサンプルコードがなかなかマッチせずいつも四苦八苦しています。どちらかというと、商用プログラムのほとんどは、UIインプット型より、データ更新型だと思うのですが…

さて今回、以下のような6個のボタンからなるカタマリが、上下方向にスクロールすることを実現したいと思います。

| A00 | A10 | A20 |
| A01 | A11 | A21 |

| B00 | B10 | B20 |
| B01 | B11 | B21 |

CollectionViewを使いますが、CollectionViewに登録するカタマリについては将来増やす可能性があるので、XAMLでスタティックに記述しません。XAMLは中身がない形になります。

MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.MainPage">
	<ContentPage.Content>
		<CollectionView x:Name="CollectionView">
			<CollectionView.ItemTemplate>
				<DataTemplate>
				</DataTemplate>
			</CollectionView.ItemTemplate>
		</CollectionView>
	</ContentPage.Content>
</ContentPage>

https://learn.microsoft.com/ja-jp/dotnet/maui/user-interface/controls/collectionview/

CollectionViewに登録する中身はITEMになります。サンプルコードに従いCollectionViewと親和性が高いObservableCollectionを使います。

  • コンストラクタは引数をもち、Viewから初期データをもらえるようにしておきます。
  • ITEMのプロパティは、とりあえずボタン6個分です。
  • class ButtonにはTextしか用意していませんが、TextColor,FontSize等も追加できます。
ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;
	public ObservableCollection<ITEM> Items { get; private set; }
	public ViewModel(List<ITEM> source)
	{
		Items = new ObservableCollection<ITEM>(source);
	}
}

public class ITEM
{
	public ITEM(string[,] ary)
	{
		for (int c = 0; c < ary.GetLength(0); c++)
		{
			for (int r = 0; r < ary.GetLength(1); r++)
			{
				switch (c, r)
				{
					default:
					case (0, 0): Button00 = new BUTTON(ary[c, r]); break;
					case (0, 1): Button01 = new BUTTON(ary[c, r]); break;
					case (1, 0): Button10 = new BUTTON(ary[c, r]); break;
					case (1, 1): Button11 = new BUTTON(ary[c, r]); break;
					case (2, 0): Button20 = new BUTTON(ary[c, r]); break;
					case (2, 1): Button21 = new BUTTON(ary[c, r]); break;
				}
			}
		}
	}
	public BUTTON Button00 { get; set; }
	public BUTTON Button01 { get; set; }
	public BUTTON Button10 { get; set; }
	public BUTTON Button11 { get; set; }
	public BUTTON Button20 { get; set; }
	public BUTTON Button21 { get; set; }
}

public class BUTTON
{
	public BUTTON(string s = "")
	{
		Text = s;
	}
	public string Text { get; set; }
}

さて順番が逆になりましたが親玉のViewです。

  • 前述の通り、ViewModeを生成する際に画面データを渡します。
  • XAMLがほとんど空であった分DataTemplateで記述することになります。
  • btn.SetBindingButton.TextPropertyITEMのプロパティをバインドさせます。
MainPage.xaml.cs
public class MainPage : ContentPage
{
	ViewModel VM;
	public MainPage()
	{
		InitializeComponent();
		VM = new ViewModel(source);
		CollectionView.ItemsSource = VM.Items;

		DataTemplate dt = new DataTemplate(() =>
		{
			var cd = new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) };
			var rd = new RowDefinition { Height = new GridLength(1, GridUnitType.Star) };
			Grid grid = new Grid
			{
				ColumnDefinitions = { cd, cd, cd },
				RowDefinitions = { rd, rd }
			};
			for (int c = 0; c < 3; c++)
			{
				for (int r = 0; r < 2; r++)
				{
					Button btn = new Button();
					Grid.SetColumn(btn, c);
					Grid.SetRow(btn, r);
					btn.SetBinding(Button.TextProperty, $"Button{c}{r}.Text");
					grid.Children.Add(btn);
				}
			}
			StackLayout layout = new StackLayout();
			layout.Children.Add(grid);
			return layout;
		});
		CollectionView.ItemTemplate = dt;
	}
	//画面データ
	List<ITEM> source = new List<ITEM>
	{
		new ITEM(new string[,] { {"A00", "A01"}, {"A10", "A11"}, {"A20", "A21"} }),
		new ITEM(new string[,] { {"B00", "B01"}, {"B10", "B11"}, {"B20", "B21"} }),
		new ITEM(new string[,] { {"C00", "C01"}, {"C10", "C11"}, {"C20", "C21"} }),
		new ITEM(new string[,] { {"D00", "D01"}, {"D10", "D11"}, {"D20", "D21"} }),
		new ITEM(new string[,] { {"E00", "E01"}, {"E10", "E11"}, {"E20", "E21"} }),
		new ITEM(new string[,] { {"F00", "F01"}, {"F10", "F11"}, {"F20", "F21"} }),
		new ITEM(new string[,] { {"G00", "G01"}, {"G10", "G11"}, {"G20", "G21"} }),
		new ITEM(new string[,] { {"H00", "H01"}, {"H10", "H11"}, {"H20", "H21"} })
	};
	public void OnUpdate()
	{
		//これでは更新されない
		VM.Items[1].Button01.Text = "更新";
		
		//これなら更新できるが、これでいいのか?
		var i = VM.Items[1];
		i.Button01.Text = "更新";
		VM.Items[1] = i;
	}
}

Windowsアプリとして動作させると下記のような結果になります。

![アプリキャプチャ](https://storage.googleapis.com/zenn-user-upload/ccdc5476d8d4-20230909.png =250)

CollectionViewの要素を部分的に更新する

さてこの実装に対して、ボタンのTextを書き換えようとすると、OnUpdate()のようなインターフェースを用意することになろうかと思うのですが、コメントにありますようにTextのみの更新はうまくいかず、よくわからないけどITEMをまるごと入れ替えれば、更新されることはわかっていました。何か違和感はありましたが、とりあえず動いていたので放置していましたが、OSによっては、無駄に全体が再描画されているようにも見え、残念ながら実用に耐えられる感触は得られないままでした。

今回.NET MAUIへの移行してみると、事態は深刻化しており、無駄な再描画に加えて、先頭へのスクロールも発生、ITEMを入れ替える方法は使い物にならないことがはっきりしました。解決策を模索していると、下記のような記事に巡り合い、

https://water2litter.net/rye/post/c_inotify_propertychanged/

そもそもCollectionViewの実装に出てくるObservableCollectionは、追加や削除でしか動作せず、コレクションされた要素の変更は別途実装しなくてはならない旨が指摘されておりました。具体的には更新したいプロパティのsetNotifyPropertyChanged()を付加すれば良いという指摘です。

この指摘を受けclass BUTTONを下記のように書き直し、無事Textのみを更新することに成功しました。作者の方には、この場を借りて厚く御礼申し上げます。

ViewModel.cs (一部)
public class BUTTON : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;
	private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
	{
		if (PropertyChanged != null)
		{
			PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}
	}
	public BUTTON(string s = "")
	{
		Text = s;
	}
	string _text;
	public string Text {
		get {
			return _text;
		}
		set {
			_text = value;
			NotifyPropertyChanged();
		}
	}
}

(了)

Discussion