🍐

XAMLで2次元配列をバインディングできないの?

2023/09/16に公開

https://zenn.dev/kariki_no10/articles/456b06037d7c4f

「CollectionViewの要素は、部分的に更新できないものなのか?」で紹介したコードでは、Button00Button21の記述が冗長で、この程度の数なら良いのですが、要素が増えてくると5x5くらいでも大変な記述量になってくるという問題があります。Button[0][0]と配列化するのが筋ですが、XAMLを使うにはバインディング対象をプロパティで用意しなくてはならず、いったい配列のgetter/setterってどう記述するの?という難問に突き当たります。

インデクサで配列を表現

実は、配列のプロパティは可能で、公式にインデクサといわれる手法が紹介されています。

https://learn.microsoft.com/ja-jp/dotnet/csharp/indexers

ドキュメントは小難しくてとても読めたものではありませんが、書き下してみるとそう難しくはありません。

ViewModel.cs (一部)
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++)
			{
				this[c, r] = new BUTTON(ary[c, r]);
			}
		}
        }
        BUTTON[,] _buttons = new BUTTON[3, 2];
        public BUTTON this[int c, int r]
        {
		get { return _buttons[c, r]; }
		set { _buttons[c, r] = value; return; }
        }
}

当然ですがコード量は劇的に減ります。View側の変更点は、//バインディング と //値の更新 で示される2箇所になります。

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, $"[{c},{r}].Text");
					grid.Children.Add(btn);
				}
			}
			StackLayout layout = new StackLayout();
			layout.Children.Add(grid);
			return layout;
		});
		CollectionView.ItemTemplate = dt;
	}
	//画面データ
	
	(中略)
	
	public void OnUpdate()
	{
		//値の更新
		VM.Items[1][0,1].Text = "更新";
	}
}

かなりスマートに書き下せ、これが正解かと思いきや、実際動作させてみるとビルドは通るものの //バインディング のカギ括弧を解釈できないようで、警告の嵐です[1]。XAML側でカギ括弧をパースできないのでしょう。

Microsoft.Maui.Controls.Xaml.Diagnostics.BindingDiagnostics: Warning: '0,0' property not found on 'MauiApp1.ITEM', target property: 'Microsoft.Maui.Controls.Button.Text'
Microsoft.Maui.Controls.Xaml.Diagnostics.BindingDiagnostics: Warning: '0,1' property not found on 'MauiApp1.ITEM', target property: 'Microsoft.Maui.Controls.Button.Text'
Microsoft.Maui.Controls.Xaml.Diagnostics.BindingDiagnostics: Warning: '1,0' property not found on 'MauiApp1.ITEM', target property: 'Microsoft.Maui.Controls.Button.Text'
Microsoft.Maui.Controls.Xaml.Diagnostics.BindingDiagnostics: Warning: '1,1' property not found on 'MauiApp1.ITEM', target property: 'Microsoft.Maui.Controls.Button.Text'
Microsoft.Maui.Controls.Xaml.Diagnostics.BindingDiagnostics: Warning: '2,0' property not found on 'MauiApp1.ITEM', target property: 'Microsoft.Maui.Controls.Button.Text'
Microsoft.Maui.Controls.Xaml.Diagnostics.BindingDiagnostics: Warning: '2,1' property not found on 'MauiApp1.ITEM', target property: 'Microsoft.Maui.Controls.Button.Text'

Column0.Row0という記法でお茶を濁す

  • //バインディング
    $"[{c},{r}].Text"$"Column{c}.Row{r}.Text"

となるようにclass COLUMNというのを一枚介在させ、setterで逐一コピーします。あまり面白くない結論ですが、現状これが精いっぱいかなと思います。将来.NETがインデクサもサポートすることを切に望みます。

ViewModel.cs (一部)
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++)
			{
				this[c, r] = new BUTTON(ary[c, r]);
			}
		}
        }
        BUTTON[,] _buttons = new BUTTON[3, 2];
        public BUTTON this[int c, int r]
        {
		get { return _buttons[c, r]; }
		set { 
			_buttons[c, r] = value;
			switch (c, r)
			{
				default:
				case (0, 0): Column0.Row0 = value; break;
				case (0, 1): Column0.Row1 = value; break;
				case (1, 0): Column1.Row0 = value; break;
				case (1, 1): Column1.Row1 = value; break;
				case (2, 0): Column2.Row0 = value; break;
				case (2, 1): Column2.Row1 = value; break;
			}
			return; 
		}
        }
	public COLUMN Column0 { get; set; } = new COLUMN();
	public COLUMN Column1 { get; set; } = new COLUMN();
	public COLUMN Column2 { get; set; } = new COLUMN();
}

public class COLUMN
{
	public BUTTON Row0 { get; set; }
	public BUTTON Row1 { get; set; }
}

(了)

脚注
  1. class ITEMのインデクサthisが気持ち悪いので、ITEMButtonsというメンバを介在させて、$"[{c},{r}].Text"$"Buttoms[{c},{r}].Text"VM.Items[1][0,1].Text = "更新"; → VM.Items[1].Buttons[0,1].Text = "更新"; となるような変更を加えてもうまくいきません。 ↩︎

Discussion