読者です 読者をやめる 読者になる 読者になる

KatsuYuzuのブログ

.NET の開発をメインとした日記です。

Silverlight5×WCF RIA Services×Reactive ExtensionsでTreeViewをいじってみた。

ちょっと疑問に思って投げっぱなしだったのを形にしました。が、少し詰め込みすぎてやりたかったことに辿りつくのに時間が……。
疑問に思ってたこと(=やりたかったこと)は「ツリー構造のIEnumerableをLINQで展開したり、要素を探したりしたかった」です。

色々オマケがついてますが、該当部分は下記の1ファイルのみです。

  • \TreeSample\Infrastructures\IEnumerableExtensions.cs

実行画面

プロジェクト


TreeSample.zip

Silverlight5の新機能

Silverlight5ではStyleのSetterにBindingが使えるようになりました。これによってTreeViewItemの展開、選択をViewModelから制御出来るようになりました。
(ちなみに、Silverlight4のときは条件をバインドして展開、選択を行うビヘイビアーで無理に対応してました。)

        <!-- Silverlight5からの機能: StyleのSetterにBindingが使えるようになった -->
        <Style x:Key="NodeItemStyle" TargetType="sdk:TreeViewItem">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
        </Style>

HierarchicalDataTemplateでツリー構造なオブジェクトを表現する

ツリー構造の表現にはHierarchicalDataTemplateを使用します。HierarchicalDataTemplateのItemsSourceに子要素を指定します。
そのHierarchicalDataTemplateをTreeViewのItemTemplateに指定すると、TreeViewのItemsSourceからツリー構造を辿って表示されます。

        <!-- ツリー構造を形成する際はHierarchicalDataTemplateを使い、NodeとなるItemsSourceを指定する -->
        <sdk:HierarchicalDataTemplate x:Key="TreeViewItemTemplate" ItemsSource="{Binding Nodes}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding}"/>
                <TextBlock Text=" ← いまここ">
                    <!-- 表示、非表示の制御はDataTriggerで行う(boolをコンバーターでバインドするのもいいけどDataTriggerを覚えておくと色々対応出来る) -->
                    <i:Interaction.Triggers>
                        <ei:DataTrigger Binding="{Binding IsSelected}" Value="True">
                            <ei:ChangePropertyAction PropertyName="Visibility">
                                <ei:ChangePropertyAction.Value>
                                    <Visibility>Visible</Visibility>
                                </ei:ChangePropertyAction.Value>
                            </ei:ChangePropertyAction>
                        </ei:DataTrigger>
                        <ei:DataTrigger Binding="{Binding IsSelected}" Value="False">
                            <ei:ChangePropertyAction PropertyName="Visibility">
                                <ei:ChangePropertyAction.Value>
                                    <Visibility>Collapsed</Visibility>
                                </ei:ChangePropertyAction.Value>
                            </ei:ChangePropertyAction>
                        </ei:DataTrigger>
                    </i:Interaction.Triggers>
                </TextBlock>
            </StackPanel>
        </sdk:HierarchicalDataTemplate>

LINQでツリー構造なIEnumerableを展開

匿名メソッドを定義して再帰的に呼び出してConcatで連結して〜みたいなことをやってみました。
ちなみに、LINQのix(実験的な実装でまだ正式じゃないよ的な?)には、まさしくExpandという拡張メソッドがあるみたいです。

        public static IEnumerable<T> Expand<T>(this IEnumerable<T> self, Func<T, IEnumerable<T>> collectionSelector)
        {
            Func<T, IEnumerable<T>> func = null;
            func = source =>
            {
                if (collectionSelector(source).Count() == 0)
                {
                    return Enumerable.Empty<T>();
                }
                else
                {
                    return Enumerable.Concat(collectionSelector(source), collectionSelector(source).SelectMany(x => func(x)));
                }
            };

            return Enumerable.Concat(self, self.SelectMany(x => func(x)));
        }

で、本当にやりたかったのはこの中から特定要素を見つけることです。こんな風に直してみました。

        public static T Search<T>(this IEnumerable<T> self, Func<T, bool> selector, Func<T, IEnumerable<T>> collectionSelector)
        {
            var isFound = false;

            Func<T, IEnumerable<T>> func = null;
            func = source =>
            {
                if (selector(source))
                {
                    isFound = true;
                }

                if (isFound || collectionSelector(source).Count() == 0)
                {
                    return Enumerable.Empty<T>();
                }
                else
                {
                    return Enumerable.Concat(collectionSelector(source), collectionSelector(source).SelectMany(x => func(x)));
                }
            };

            return Enumerable.Concat(self, self.SelectMany(x => func(x)))
                .Where(selector)
                .FirstOrDefault();
        }

WCF RIA Servicesの非同期通信をReactive Extentisonsで流れるように取得する

題材に使うWCF RIA Servicesの処理を定義していきます。

階層構造を持ったEntityにしたかったので外部キーを属性で定義しています。この属性をつけてクライアントに戻すといい感じにやっといてくれます(便利!)。

    public class NodeItem
    {
        public NodeItem() { }

        public NodeItem(int parentNodeid, int nodeId)
            : this()
        {
            this.ParentNodeId = parentNodeid;
            this.NodeId = nodeId;
        }

        [Key]
        public int NodeId { get; set; }

        public int ParentNodeId { get; set; }

        [Include]
        [Association("FK_NodeItem_NodeItem", "NodeId", "ParentNodeId")]
        public IEnumerable<NodeItem> Nodes { get; set; }
    }

テストデータとテスト時間をでっちあげて返します。

        public IEnumerable<NodeItem> GetTree()
        {
            // 時間がかかる処理を偽装
            System.Threading.Thread.Sleep(1000);

            // 適当なデータ(FKを張ったRDBを想定)
            var data = new NodeItem[] { 
                        new NodeItem(0, 0), // FK違反対策のdummy root
                        new NodeItem(0, 1), // 画面上のroot
                        new NodeItem(1, 2),
                        new NodeItem(1, 3),
                        new NodeItem(1, 4),
                        new NodeItem(2, 5),
                        new NodeItem(3, 6),
                        new NodeItem(3, 7),
                        new NodeItem(4, 8),
                        new NodeItem(4, 9),
                        new NodeItem(5, 10),
                        new NodeItem(5, 11),
                        new NodeItem(6, 12),
                        new NodeItem(6, 13),
                        new NodeItem(7, 14),
                        new NodeItem(9, 15),
                        new NodeItem(15, 16),
                        new NodeItem(10, 17),
                        new NodeItem(17, 18),
                        new NodeItem(11, 19),
                        new NodeItem(19, 20)};

            // とりあえず返却 → RIA Servicesの属性の仕組みでTree構造に。
            return data.Where(x => x.NodeId > 0);
        }

次はクライアントです。

拡張メソッドを使ってDomainContextのLoadを呼び出します。拡張メソッドからはIEnumerableが1つの要素として流れてくるので、平坦にして分かりやすく(?)扱えるように流してみます。
(拡張メソッドは記事の最後の参考、またはソースで参照してください。)

        public IObservable<NodeItem> GetTree()
        {
            return _treeSampleContext.LoadAsObservable(_treeSampleContext.GetTreeQuery())
                .Select(x => x.ToObservable()) // Switch用に変換
                .Switch() // IObservable<IObservable<T>>を平坦に(?)
                .Where(x => x.ParentNodeId == 0); // root直下のみを返す
        }

ここまでをAPIとして封印しているので呼び出す方はとてもシンプルです。
例外が起きた場合にSubscribeのFinallyに処理がこないので、例外部分でもIsBusyを制御する必要があります。例外を画面に出す出さないはアプリの集約例外で掴んでViewで制御したいので投げちゃいます。)

        public void Load()
        {
            IsBusy = true;

            Nodes.Clear();

            _api.GetTree()
                .Subscribe(node =>
                {
                    Nodes.Add(node);
                },
                ex =>
                {
                    IsBusy = false;
                    IsLoaded = true;
                    throw ex;
                },
                () =>
                {
                    IsBusy = false;
                    IsLoaded = true;
                });
        }

INotifyPropertyChangedの実装

色々な実装方法がありますが、私はこの実装が好きです。
式木からプロパティ名を取り出すことでソース中のリテラル排除できます。更にstatic readonlyな変数でキャッシュしていて1度だけの実行なのでパフォーマンスも気になりません。
(拡張メソッドは記事の最後の参考、またはソースで参照してください。)

        public bool IsBusy
        {
            get
            {
                return _isBusy;
            }
            private set
            {
                if (value == _isBusy) return;
                _isBusy = value;
                PropertyChanged.Raise(this, IsBusyName);
            }
        }
        private bool _isBusy;
        internal static readonly string IsBusyName = PropertyName<TreeSampleApplication>.Get(_ => _.IsBusy);

さらに、PropertyChangedの購読側でもキャッシュしたプロパティ名を使えるので、ここでもリテラル排除できます。

            TreeSampleApplication.Current.PropertyChanged += (sender, e) =>
                {
                    if (e.PropertyName == TreeSampleApplication.IsLoadedName)
                    {
                        TreeSampleApplication.Current.Nodes
                            .ToList()
                            .ForEach(x => Nodes.Add(new NodeItemViewModel(null, x)));
                    }
                    else if (e.PropertyName == TreeSampleApplication.IsBusyName)
                    {
                        IsBusy = TreeSampleApplication.Current.IsBusy;
                    }
                };