かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

UWP の TreeView でデータバインディングを使ってデータを表示する(プレビュー)

Windows 10 April 2018 Update で追加された TreeView ですがデータバインディングでデータを表示することが出来なくて不満でしたが、今見たらプレビューで出来るようです。

ちょっと試してみました。使い方は簡単です。ItemsSource にコレクションをバインドするのと、ItemTemplate 内で TreeViewItem の ItemsSource に子要素をバインドすることです。

因みに TreeView は仮想化されてないみたいなので、アプリ開発者側で頑張っていらないものは消してねってスタンスみたいです。じゃぁやってみましょう。

表示するデータとして以下のような Item クラスを準備しました。

using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace App1
{
    public class Item : BindableBase
    {
        private CancellationTokenSource _cancellationTokenSource;
        private bool _hasUnrealizedChildren;
        public bool HasUnrealizedChildren
        {
            get { return _hasUnrealizedChildren; }
            set { SetProperty(ref _hasUnrealizedChildren, value); }
        }

        private bool _isLoading;
        public bool IsLoading
        {
            get { return _isLoading; }
            set { SetProperty(ref _isLoading, value); }
        }

        private string _text;
        public string Text
        {
            get { return _text; }
            set { SetProperty(ref _text, value); }
        }

        public ObservableCollection<Item> Children { get; } = new ObservableCollection<Item>();

        private static int _counter;

        public static Item Create() => new Item
        {
            Text = $"Item {_counter++}",
            HasUnrealizedChildren = true,
        };

        public async Task FillChildrenAsync()
        {
            if (IsLoading)
            {
                return;
            }

            IsLoading = true;
            _cancellationTokenSource = new CancellationTokenSource();
            for (int i = 0; i < 5; i++)
            {
                await Task.Delay(1000, _cancellationTokenSource.Token);
                if (_cancellationTokenSource.IsCancellationRequested)
                {
                    break;
                }

                Children.Add(Create());
            }

            IsLoading = false;
            HasUnrealizedChildren = false;
        }

        public Task ClearChildrenAsync()
        {
            _cancellationTokenSource?.Cancel();
            Children.Clear();
            IsLoading = false;
            HasUnrealizedChildren = true;
            return Task.CompletedTask;
        }
    }
}

BindableBase クラスは Prism.Core のパッケージのものです。自分で INotifyPropertyChanged を実装するのがめんどくさかったので使ってます。FillChildrenAsync で子要素を順次読み込んで、ClearChildrenAsync で子要素を綺麗にするようにしてます。

あとは、MainPage.xaml.cs のプロパティとして定義して、TreeView のノードを展開したときと閉じたときのイベントをハンドリングして対象ノードのデータの子要素を読み込んだりクリアーしたりしてます。

<Page
    x:Class="App1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

    <Grid>
        <TreeView x:Name="treeView" 
                  ItemsSource="{x:Bind Items}"
                  Expanding="{x:Bind TreeView_Expanding}"
                  Collapsed="{x:Bind TreeView_Collapsed}">
            <TreeView.ItemTemplate>
                <DataTemplate x:DataType="local:Item">
                    <TreeViewItem ItemsSource="{x:Bind Children}"
                                  HasUnrealizedChildren="{x:Bind HasUnrealizedChildren, Mode=OneWay}">
                        <StackPanel Orientation="Horizontal">
                            <ProgressRing Visibility="{x:Bind local:BindingUtilities.BoolToVisibility(IsLoading), Mode=OneWay}"
                                          IsActive="True" />
                            <TextBlock Text="{x:Bind Text}"
                                       Style="{ThemeResource BodyTextBlockStyle}" />
                        </StackPanel>
                    </TreeViewItem>
                </DataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Page>
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// 空白ページの項目テンプレートについては、https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x411 を参照してください

namespace App1
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        private IEnumerable<Item> Items { get; } = Enumerable.Range(1, 10).Select(_ => Item.Create()).ToArray();
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void TreeView_Expanding(TreeView sender, TreeViewExpandingEventArgs args)
        {
            (args.Item as Item)?.FillChildrenAsync();
        }

        private void TreeView_Collapsed(TreeView sender, TreeViewCollapsedEventArgs args)
        {
            (args.Item as Item)?.ClearChildrenAsync();
        }
    }
}

あとは、bool から Visibility への変換用の関数も用意しました。x:Bind 楽ですね。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;

namespace App1
{
    public static class BindingUtilities
    {
        public static Visibility BoolToVisibility(bool value) => BoolToVisibility(value, true);

        public static Visibility BoolToVisibility(bool value, bool trueIsVisible)
        {
            if (trueIsVisible)
            {
                return value ? Visibility.Visible : Visibility.Collapsed;
            }

            return value ? Visibility.Collapsed : Visibility.Visible;
        }
    }
}

実行するとこんな感じで動きます。

f:id:okazuki:20180927164436g:plain

よきかな。