MAUI の CollectionView を使ってツリーを表示するために、 SelectedIndex を取得する添付ビヘイビアを作ったりして頑張ったが、ListView を使えばそんな努力不要だった。
ListView には SelectedItemIndex プロパティがあるので、これを普通に使えば済む。
using System.Collections.ObjectModel; using CommunityToolkit.Maui.Markup; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.Extensions.Logging; namespace TreeSample; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp<App>() .UseMauiCommunityToolkitMarkup() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); #if DEBUG builder.Logging.AddDebug(); #endif return builder.Build(); } } public class App : Application { public App() { MainPage = new MainPage(); } } public class MainPage : ContentPage { private readonly MainViewModel _viewModel; public MainPage() { BindingContext = _viewModel = new MainViewModel(); // サンプルデータ for (var i = 0; i < 5; i++) { var node = new TreeNode { Level = 0, Name = $"{i}", }; _viewModel.TreeNodes.Add(node); for (var j = 0; j < 5; j++) { var subNode = new TreeNode { Level = 1, Name = $"{i}.{j}", }; node.Children.Add(subNode); for (var k = 0; k < 5; k++) { var subSubNode = new TreeNode { Level = 2, Name = $"{i}.{j}.{k}", }; subNode.Children.Add(subSubNode); } } } Content = new ListView { SelectionMode = ListViewSelectionMode.Single, ItemsSource = _viewModel.TreeNodes, ItemTemplate = new DataTemplate(() => { return new ViewCell { View = new HorizontalStackLayout { new Label() .Bind( Label.TextProperty, path: nameof(TreeNode.IsOpened), convert:(bool x)=> x ? "-" : "+") .Bind( Label.IsVisibleProperty, path: nameof(TreeNode.IsLeaf), convert: (bool x) => !x), new Label() .Bind( Label.TextProperty, path: nameof(TreeNode.Name)), } .Bind( HorizontalStackLayout.PaddingProperty, path: nameof(TreeNode.Level), // ネストが深いほど左のマージンを増やす convert: (int x) => new Thickness(left: 10 + 10 * x, right: 10, top: 10, bottom: 10)), }; }), } .Invoke(x => x.ItemSelected += (sender, e) => { _viewModel.Select(e.SelectedItemIndex); if (sender is ListView listView) { listView.SelectedItem = null; } }); } } public partial class MainViewModel : ObservableObject { public ObservableCollection<TreeNode> TreeNodes { get; } = new(); public void Select(int index) { if (index < 0 || TreeNodes.Count <= index) { return; } var node = TreeNodes[index]; if (node.IsOpened) { // 選択したアイテムより後かつ、 // 自分より深いノードを削除することで、 // 閉じたように振舞う。 var i = index + 1; while (i < TreeNodes.Count) { var subNode = TreeNodes[i]; if (subNode.Level > node.Level) { subNode.IsOpened = false; TreeNodes.RemoveAt(i); } else { break; } } node.IsOpened = false; } else { // 自分の後ろにノードを挿入することで // 開いたように振舞う。 for (var i = 0; i < node.Children.Count; i++) { var insertIndex = index + 1 + i; TreeNodes.Insert(insertIndex, node.Children[i]); } node.IsOpened = true; } } } public partial class TreeNode : ObservableObject { [ObservableProperty] private int _level; [ObservableProperty] private bool _isOpened; [ObservableProperty] private string? _name; public ObservableCollection<TreeNode> Children { get; } = new(); public bool IsLeaf => Children.Count == 0; }
どうしても CollectionView が必要な場面でもない限りは、これでいいや。随分と廻り道をしてしまったな。