この記事について
Unity6/UIToolKitでListViewのランタイムデータバインディングを コードだけで 実践するする方法を書いた記事です。
UIBuilderからいろいろ操作して……みたいな方法はUnityの公式ドキュメント等に記載がありますが、コードベースで実践するものはあまり見かけないので備忘録的に残したものです。
UIBuilderをごにょごにょ触るのではなく、コードベースでバインディングを実現したい理由としては
- 変更があった時に.uxmlより.csの方が差分が見やすい!
- UIBuilderをごにょごにょ触るのがめんどい!
- コーディングとデザインを完全に分離したい!
みたいな個人的な気持ちがあります。
できたもの
動画だと何がおきてるk全然わからん!!!!!!!!!!!!!!
コード
ViewModel:
View:
解説
用語
- itemTemplate
- ListView.itemTemplate
- ListViewに並ぶUIのテンプレート
- VisualTreeAssetで指定
- ListView.makeItemでコードから作ることも可能
- itemsSource
- ListView.itemsSource
- ListViewにUIを並べるときに利用するデータソース
- itemsSourceの中身一つがインスタンス化されたitemTemplate一つにバインドされる
ViewModel側
Unity上でPlayを押下した際のコードの流れは次のようになっています。
- ListViewTestingViewModelがコンストラクタでListViewTestingViewModel.Data のリストを作成する(DataList)
- ListViewTestingView が Sart() でバインディングを実施する
- UI上にDataListの数だけラベルが並ぶ
ListViewTestingViewModel
はitemsSourceになるDataListを持つViewModelです。
DataListは次のように宣言されています。
[CreateProperty] public List<Data> DataList { get; private set; }
[CreateProperty]
属性は、UIElement側とC#のコードの間で変更の通知を送り合うために必要な属性です。
この属性が付いているメンバがバインド先として指定(後述)されることによって、データの変更に応じてUIを変更する…ということが可能になります。
今回のListViewでは、DataListの要素に応じてテンプレートに指定したUI要素が追加されていきます。
同様に、ListViewTestingViewModel.Data ではUI上に表示される文字列を次のように宣言しています。
[CreateProperty] public string Label { get; }
View側
ViewはシンプルなMonoBehaviourとしています。
GitHub上にはListViewTesting.unityというサンプルシーンを用意してあるので、そちらを確認しながらコードを追ってもらうといいかもです。
Viewでは次の内容を宣言しています。
private UIDocument _uiDocument; // ListViewを配置しただけのシンプルなVisualTreeAssetを指定
private ListView _listView; // 今回触るListView
private readonly ListViewTestingViewModel _viewModel = new(); // ViewModelの実体
[SerializeField] private VisualTreeAsset templateAsset; // DataListの各要素に対応するテンプレート
templateAssetに指定したVisualTreeAssetをインスタンス化し、そのVisualElementに対してDataListの要素一つがバインディングされます。
実際のバインディング処理はStart()内で実施しています。
private void Start()
{
_listView = _uiDocument.rootVisualElement.Q<ListView>();
_listView.dataSource = _viewModel;
// itemsSourceの要素とitemTemplateから
// インスタンス化されたVisualElementをどのように
// バインディングするかを記述する
_listView.bindItem += (element, index) =>
{
element.dataSource =_viewModel.DataList[index];
var label = element.Q<Label>();
label.SetBinding(
bindingId: nameof(Label.text),
binding: new DataBinding()
{
dataSourcePath = PropertyPath.FromName(nameof(ListViewTestingViewModel.Data.Label))
}
);
};
_listView.itemTemplate = templateAsset;
// itemsSourceに対して、ListViewTestingViewModel.DataListを指定
_listView.SetBinding(
bindingId: nameof(ListView.itemsSource),
binding: new DataBinding()
{
dataSourcePath = PropertyPath.FromName(nameof(ListViewTestingViewModel.DataList))
}
);
// 本来はunbindItemも実装する必要があるが、サンプルのため割愛
}
コードベースでのバインディングについて
ListView.bindItemはitemsSourceの要素一つと、インスタンス化されたitemTemplateをバインディングするためのコールバックです。
今回のサンプルでは、ListViewBindingTestingViewModel.Data.Labelが、itemTemplate上のLabelに対してバインディングされるよう指定しています。
コードベースからバインディングするには
- dataSourceの指定
- SetBindingでBinding対象の指定
が必要です。
element.dataSource =_viewModel.DataList[index];
label.SetBinding(
bindingId: nameof(Label.text),
binding: new DataBinding()
{
dataSourcePath = PropertyPath.FromName(nameof(ListViewTestingViewModel.Data.Label))
}
);
-
dataSource
- バインディングされるデータ
- サンプルでは element.dataSource = _viewModel.DataList[index]; と指定
- これによって、インスタンス化されたitemTemplateに対して、_viewModel.DataList[index]がデータとして設定される
-
bindingId
- VisualElementが持つどのプロパティに対してバインディングするか
- コード上のプロパティ名を指定
- ここではUnityEngine.UIElements.Label をバインディングするプロパティとして指定
- コード上のプロパティ名を指定
- (文字列での指定は PropertyPath.FromName()と同等)
- VisualElementが持つどのプロパティに対してバインディングするか
-
binding
- どのように、何をバインディングするか
- bindingIdと同様に、コード上のプロパティ名を指定する
- どのように、何をバインディングするか
ここまで記述してEditorから実行すると、ListView上にDataListの要素数分Guidが並びます。
要素を追加する
ListViewTestingViewModel.Add() では要素を追加するサンプルが記述されています。
https://github.com/fireskyvvv/Unity6RuntimeBindingTesting/blob/master/Assets/RuntimeBindingTesting/ListViewTesting/ListViewTestingViewModel.cs#L30-L36
public void Add()
{
DataList.Add(new Data(System.Guid.NewGuid().ToString()));
DataList = DataList.ToList();
DebugLog($"New data added to List. Current count:{DataList.Count}");
}
DataListには[CreateProperty]
属性が付いているので、
- ここの参照が新たなListに変わる
- ListView.itemsSourceに変更通知が飛ぶ
- UIに新たな要素が追加される
という感じで要素追加が実現されます。
DataList = DataList.ToList(); ←!?
DataList = DataList.ToList();
という不思議な記述がありますが、これはDataListに変更があったことを通知するための苦肉の策です。
Observable的に変更通知が送れればいいのですが、どうにもなさそう…?
INotifyPropertyChanged の実装等も試したのですが…… 情報ください……
感想
Xamlとかになれている身からすると、どうしてもコード上でプロパティ名を指定してバインディング対象を指定…したくなってしまうので、こういう感じの実装の方法を模索したりしてます。
また、コード上でのバインディングをやることによって、UIロジックの実装をしつつデザインの更新をお願いする…みたいな感じでコーディングとデザインを分離することができそうでよきなのでは…!?と思ったりしてます。
(実際はそう上手くいかない場面も多々あるんだろうけど……)
まだまだ情報が少ないので、もっとたくさんUIToolKit知見が出回ってほしいな~~~
おわり