none
HierarchicalDataTemplateとCompositeCollectionの組み合わせについて RRS feed

  • 質問

  • HierarchicalDataTemplateを使ってツリー構造を表示しており、
    普通に表示するところまでは正しくできております。
    続いて、ModelViewに、管理しているコレクションをある基準に従って分類する機能を持たせました。
    ツリーを表示する際に、この分類方法に対応する階層を1階層挿入して表示させたいのですが
    うまく動作せず困っております。

    具体的には、1つだけならうまく動いているように見えるのですが、
    同じテンプレートに従う別の項目を展開すると、それまで正しく表示されていた
    部分が真っ白に戻ってしまう、という症状です。

    いろいろ試してみましたが、恐らく
    「挿入した1階層」がページ全体に対して1つしか生成されていない、
    ように思えます。

    以下が再現コードです。

    //MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Branch left = new Branch( true );
            Branch right = new Branch( false );

            left.Branches.Add( new Branch( true ) );
            left.Branches.Add( new Branch( false ) );

            right.Branches.Add( new Branch( true ) );
            right.Branches.Add( new Branch( false ) );

            DataContext = new Branch[]{ left, right };
        }
    }

    public class Branch
    {
        private static int count = 0;

        public bool IsLeft
        {
            get;
            private set;
        }

        public string Text
        {
            get;
            private set;
        }

        private List<Branch> branches = new List<Branch>();
        public List<Branch> Branches
        {
            get { return branches; }
        }

        public IEnumerable<Branch> Left
        {
            get { return branches.Where( branch => branch.IsLeft ); }
        }

        public IEnumerable<Branch> Right
        {
            get { return branches.Where( branch => !branch.IsLeft ); }
        }

        public Branch( bool isLeft )
        {
            IsLeft = isLeft;
            Text = ( isLeft ? "L:" : "R:" ) + count++;
        }
    }

    //<!-- MainWindow.xaml -->
    <Window x:Class="TreeViewTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:TreeViewTest"
            Title="MainWindow" Height="350" Width="525">

        <Window.Resources>

            <HierarchicalDataTemplate DataType="{x:Type local:Branch}" >
                <HierarchicalDataTemplate.ItemsSource>
                    <Binding>
                        <Binding.Source>
                            <CompositeCollection>
                                <TreeViewItem Header="右の枝" ItemsSource="{Binding Right}" />
                                <TreeViewItem Header="左の枝" ItemsSource="{Binding Left}" />
                            </CompositeCollection>
                        </Binding.Source>
                    </Binding>
                </HierarchicalDataTemplate.ItemsSource>
                <TextBlock Text="{Binding Text}"/>
            </HierarchicalDataTemplate>

            <!--<HierarchicalDataTemplate DataType="{x:Type local:Branch}" ItemsSource="{Binding Branches}">
                <TextBlock Text="{Binding Text}"/>
            </HierarchicalDataTemplate>-->

        </Window.Resources>

        <StackPanel Orientation="Vertical">
            <TreeView ItemsSource="{Binding}" />
        </StackPanel>
    </Window>

    BranchクラスのBranchesには、Branchのリストが入っていますが
    Leftプロパティ, Rightプロパティを通すと
    内部属性によって分類されたBranchのリストが得られます。

    複雑な方のテンプレートの意図としては
    コメントアウトしてあるテンプレートで普通に表示される、
    以下のようなデータ構造

    L:0
     ├ R:3
     └ L:2

    を、

    L:0
     ├ 右の枝
      │  └ R:3
     └ 左の枝
        └ L:2

    のように、「右の枝」「左の枝」という、

    分類名に相当するノードを挿入して表示したい、というものです。

    色々調べた結果、CompositeCollectionなどを使っていますが、
    方向性として正しいのかどうかも自信がありません。

    どのようにテンプレートを記述すれば、上手く動作するでしょうか?
    宜しくお願い致します。

                            
    2012年7月24日 5:13

回答

  • とりあえず記載された例のような表示ができたのでサンプル貼っておきます。

        public class Branch
        {
            private static int count = 0;
    
            public bool IsLeft
            {
                get;
                private set;
            }
    
            public string Text
            {
                get;
                private set;
            }
    
            private List<Branch> branches = new List<Branch>();
            public List<Branch> Branches
            {
                get { return branches; }
            }
    
            public IEnumerable<Branch> Left
            {
                get { return branches.Where(branch => branch.IsLeft); }
            }
    
            public IEnumerable<Branch> Right
            {
                get { return branches.Where(branch => !branch.IsLeft); }
            }
    
            public Branch(bool isLeft)
            {
                IsLeft = isLeft;
                Text = (isLeft ? "L:" : "R:") + count++;
            }
    
            public CollectionView BranchesView 
            {
                get
                {
                    ListCollectionView collectionView = new ListCollectionView(this.Branches);
                    collectionView.GroupDescriptions.Add(new PropertyGroupDescription("IsLeft"));
                    return collectionView;
                }
            }
        }


    <Window x:Class="TreeViewTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:TreeViewTest"
            Title="MainWindow" Height="350" Width="525">
    
        <Window.Resources>
            <HierarchicalDataTemplate DataType="{x:Type CollectionViewGroup}" ItemsSource="{Binding Items}">
                <TextBlock>
                    <TextBlock.Style>
                        <Style TargetType="TextBlock">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Path=Name}" Value="True">
                                    <Setter Property="Text" Value="左の枝"/>
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Path=Name}" Value="False">
                                    <Setter Property="Text" Value="右の枝"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </TextBlock.Style>
                </TextBlock>
            </HierarchicalDataTemplate>
    
            <HierarchicalDataTemplate DataType="{x:Type local:Branch}" ItemsSource="{Binding BranchesView.Groups}">
                <TextBlock Text="{Binding Text}"/>
            </HierarchicalDataTemplate>
    
        </Window.Resources>
    
        <StackPanel Orientation="Vertical">
            <TreeView ItemsSource="{Binding}" />
        </StackPanel>
    </Window>

    何をやったかというと、
    ・ViewModel側(Branch)にBranchesのListCollectionViewを返却するアクセサ追加。
    ・その際にGroupDescription(グループ化条件)としてIsLeftプロパティを設定。
    ・HierarchicalDataTemplateをBranch用とCollectionViewGroup用に分ける。
    というところでしょうか。

    なお、私はViewModelにアクセサを追加しましたが、ここはコンバータでもなんでもOKですので。

    • 編集済み みっと 2012年7月25日 2:31
    • 回答としてマーク dandanmyan 2012年7月25日 6:43
    2012年7月25日 2:25

すべての返信

  • 最終的にどういう形にしたいのかがもうひとつ良く見えないので、とりあえず

    >>のように、「右の枝」「左の枝」という、分類名に相当するノードを挿入して表示したい、というものです。

    という表示についてのみお答えします。

    <Window x:Class="TreeViewTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:TreeViewTest"
            Title="MainWindow" Height="350" Width="525">
    
        <Window.Resources>
            <local:BranchesConverter x:Key="branchesConverter"/>
    
            <HierarchicalDataTemplate x:Key="itemTemplate" ItemsSource="{Binding Converter={StaticResource branchesConverter}}" >
                <TextBlock Text="{Binding Path=Text}"/>
            </HierarchicalDataTemplate>
    
            <HierarchicalDataTemplate x:Key="groupTemplate" ItemsSource="{Binding Converter={StaticResource branchesConverter}}" ItemTemplate="{StaticResource itemTemplate}" >
                <TextBlock>
                    <TextBlock.Style>
                        <Style TargetType="TextBlock">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Path=IsLeft}" Value="True">
                                    <Setter Property="Text" Value="左の枝"/>
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Path=IsLeft}" Value="False">
                                    <Setter Property="Text" Value="右の枝"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </TextBlock.Style>
                </TextBlock>
            </HierarchicalDataTemplate>
        </Window.Resources>
    
        <StackPanel Orientation="Vertical">
            <TreeView ItemsSource="{Binding}" ItemTemplate="{StaticResource groupTemplate}" />
        </StackPanel>
    </Window>
        class BranchesConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                Branch branch = (Branch)value;
                return branch.IsLeft ? branch.Left : branch.Right;
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }


    要するに、1階層目の(分類名を表示する)ノードと、その下のノードのテンプレートを別に定義する、ということです。
    あと、1階層目のテンプレートはItemsSourceのBinding先をIsLeftプロパティを見て動的に変更するため、コンバーターを使っています。

    それと、上記の例についてですが、確かに

    >>L:0
    >> ├ R:3
    >> └ L:2

    という形にはなりますが、これって

    >>        Branch left = new Branch( true );

    の中身がRとLに分岐してるだけで、「Branch right~」の方は表示に反映されていませんよ。
    それが意図通りなのであれば良いのですが、そのへんよくわからなかったので冒頭のように書きました。

    2012年7月24日 8:15
  • みっとさん
    回答ありがとうございます。

    テンプレートを別に定義するというアイデアをもとに考えてみたのですが、
    やりたいことを実現するためには
    x:Key="itemTemplate"の定義中に
    まだ定義されていない x:Key="groupTemplate" を参照しなければならなくなり
    うまく記述することができませんでした。
    アドバイスを役立てることができず申し訳ありません。

    また、問題点を整理しきれていなかったようなので、
    もう一度、やりたいことを正確に記載させて下さい。

    最初に投降したサンプルが適切でなかったので
    まず、MainWindowメソッドのDataContext初期化部分を変更させて下さい。

    public MainWindow()
    {
    	InitializeComponent();
    	Branch root = new Branch( true );
    	root.Branches.Add( root );
    	root.Branches.Add( new Branch( true ) );
    	root.Branches.Add( new Branch( false ) );
    	root.Branches.Add( new Branch( true ) );
    	root.Branches.Add( new Branch( false ) );
    	DataContext = new Branch[]{ root };
    }

    この状態で、

    <HierarchicalDataTemplate DataType="{x:Type local:Branch}" ItemsSource="{Binding Branches}">
                <TextBlock Text="{Binding Text}"/>
    </HierarchicalDataTemplate>

    上記テンプレートを使い、L:0 を2階層目まで開くと
    下記のような表示になると思います。

    - L:0
        - L:0
            + L:0 (このL:0は無限に同じように開くことができる)
         L:1
         R:2
         L:3
         R:4
     L:1
      R:2
      L:3
      R:4

    このようなデータ構造を、何らかのテンプレートを噛ませることによって下記のように
    表示させたいのです。

    【初期表示】
    + L:0    (*)

    【*のついたノードを開くと】
    - L:0
        + 左の枝    (*)
        + 右の枝    (*)

    【*のついたノードを開くと】
    - L:0
        - 左の枝
            + L:0    (*)
             L:1
             L:3
        - 右の枝
             R:2
             R:4

    【*のついたノードを開くと】
    - L:0
        - 左の枝
            - L:0
                + 左の枝    (*)
                + 右の枝    (*)
             L:1
             L:3
        - 右の枝
             R:2
             R:4

    【*のついたノードを開くと】
    - L:0
        - 左の枝
            - L:0
                - 左の枝
                    + L:0
                     L:1
                     L:3
                - 右の枝
                     R:2
                     R:4
             L:1
             L:3
        - 右の枝
             R:2
             R:4
    以下、同様に、L:0を開くたびに
    1階層目としては「右の枝」「左の枝」挿入され
    2階層目に分類されたBranchリストが表示される。

    ====

    宜しくお願い致します。

    2012年7月24日 10:27
  • とりあえず記載された例のような表示ができたのでサンプル貼っておきます。

        public class Branch
        {
            private static int count = 0;
    
            public bool IsLeft
            {
                get;
                private set;
            }
    
            public string Text
            {
                get;
                private set;
            }
    
            private List<Branch> branches = new List<Branch>();
            public List<Branch> Branches
            {
                get { return branches; }
            }
    
            public IEnumerable<Branch> Left
            {
                get { return branches.Where(branch => branch.IsLeft); }
            }
    
            public IEnumerable<Branch> Right
            {
                get { return branches.Where(branch => !branch.IsLeft); }
            }
    
            public Branch(bool isLeft)
            {
                IsLeft = isLeft;
                Text = (isLeft ? "L:" : "R:") + count++;
            }
    
            public CollectionView BranchesView 
            {
                get
                {
                    ListCollectionView collectionView = new ListCollectionView(this.Branches);
                    collectionView.GroupDescriptions.Add(new PropertyGroupDescription("IsLeft"));
                    return collectionView;
                }
            }
        }


    <Window x:Class="TreeViewTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:TreeViewTest"
            Title="MainWindow" Height="350" Width="525">
    
        <Window.Resources>
            <HierarchicalDataTemplate DataType="{x:Type CollectionViewGroup}" ItemsSource="{Binding Items}">
                <TextBlock>
                    <TextBlock.Style>
                        <Style TargetType="TextBlock">
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding Path=Name}" Value="True">
                                    <Setter Property="Text" Value="左の枝"/>
                                </DataTrigger>
                                <DataTrigger Binding="{Binding Path=Name}" Value="False">
                                    <Setter Property="Text" Value="右の枝"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </TextBlock.Style>
                </TextBlock>
            </HierarchicalDataTemplate>
    
            <HierarchicalDataTemplate DataType="{x:Type local:Branch}" ItemsSource="{Binding BranchesView.Groups}">
                <TextBlock Text="{Binding Text}"/>
            </HierarchicalDataTemplate>
    
        </Window.Resources>
    
        <StackPanel Orientation="Vertical">
            <TreeView ItemsSource="{Binding}" />
        </StackPanel>
    </Window>

    何をやったかというと、
    ・ViewModel側(Branch)にBranchesのListCollectionViewを返却するアクセサ追加。
    ・その際にGroupDescription(グループ化条件)としてIsLeftプロパティを設定。
    ・HierarchicalDataTemplateをBranch用とCollectionViewGroup用に分ける。
    というところでしょうか。

    なお、私はViewModelにアクセサを追加しましたが、ここはコンバータでもなんでもOKですので。

    • 編集済み みっと 2012年7月25日 2:31
    • 回答としてマーク dandanmyan 2012年7月25日 6:43
    2012年7月25日 2:25
  • みっとさん
    回答ありがとうございます。

    ViewModel側から、中間階層を表示するためのリストを出力し
    そのリストに対して別途テンプレートを適用するということですね。
    この方法を元にして、期待した通りの動作をさせることが出来ました。
    ありがとうございます。

    実際のプログラムでは、サンプルのLeft, Rightよりも
    もう少し複雑な分類を行なっていてプロパティをそのまま利用したかったため、
    下記のように実現しました。

    public class TitledList
    {
    	public string Title
    	{
    		get;
    		private set;
    	}
    	public IEnumerable<Branch> List
    	{
    		get;
    		private set;
    	}
    	public TitledList( string title, IEnumerable<Branch> list )
    	{
    		Title = title;
    		List = list;
    	}
    }
    public class Branch
    {
    	//他の部分は同じなので省略
    	public IEnumerable<TitledList> BranchesView
    	{
    		get
    		{
    			yield return new TitledList( "左の枝", Left );
    			yield return new TitledList( "右の枝", Right );
    		}
    	}
    }
    <HierarchicalDataTemplate DataType="{x:Type local:TitledList}" ItemsSource="{Binding List}">
    	<TextBlock Text="{Binding Title}" />
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate DataType="{x:Type local:Branch}" ItemsSource="{Binding BranchesView}">
    	<TextBlock Text="{Binding Text}"/>
    </HierarchicalDataTemplate>


    最初の質問についてなのですが、
    ・ViewModel側に手を加えられない場合に、同様のことをやりたい
    というのが本来の課題でした。
    ※ 読み返してみると、まったく要領を得ない質問になっていて恐縮です。

    これについては、みっとさんが仰るように、Viewの担当者がコンバータを書けば
    ViewModelに手を加えなくても実現可能ですので、課題は解決しております。
    ありがとうございます。

    それを踏まえた上で、以下は、興味本位の質問になります。

    今回の動作を、XAML側だけで完結させることはできないでしょうか?
    今のところ下記のテンプレートが正解に近い気はするのですが
    まだ上手く行っておりません。

    <!--
         Windowに名前空間を追加
            xmlns:System="clr-namespace:System;assembly=mscorlib"
            xmlns:collection="clr-namespace:System.Collections;assembly=mscorlib"
     -->
    <HierarchicalDataTemplate x:Key="template1" ItemsSource="{Binding [1].Collection}">
    	<TextBlock Text="{Binding [0]}" />
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate DataType="{x:Type local:Branch}" ItemTemplate="{StaticResource template1}">
    	<HierarchicalDataTemplate.ItemsSource>
    		<Binding>
    			<Binding.Source>
    				<collection:ArrayList>
    					<collection:ArrayList>
    						<System:String>左の枝</System:String>
    						<CollectionContainer Collection="{Binding Left}" />
    					</collection:ArrayList>
    					<collection:ArrayList>
    						<System:String>右の枝</System:String>
    						<CollectionContainer Collection="{Binding Right}" />
    					</collection:ArrayList>
    				</collection:ArrayList>
    			</Binding.Source>
    		</Binding>
    	</HierarchicalDataTemplate.ItemsSource>
    	<TextBlock Text="{Binding Text}"/>
    </HierarchicalDataTemplate>

    CollectionContainer の Collection に Bindingするところで
    DataContextが上手く参照できていないのが原因のようですが、
    うまく修正することができませんでした。

    よろしければ、アドバイス頂ければ幸いです。
    宜しくお願いします。

    2012年7月25日 6:43
  • 色々試してみたんですが、XAMLだけでは思ったような動作ができませんでした。
    要するにBranch用のHierarchicalDataTemplate.ItemsSourceを多階層のコレクションとして動的に生成できればいいわけですが、
    これがなかなか・・・(苦笑)

    とりあえず「ここは違う」と思われる部分だけ指摘しておきます。

    				<collection:ArrayList>
    					<collection:ArrayList>
    						<System:String>左の枝</System:String>
    						<CollectionContainer Collection="{Binding Left}" />
    					</collection:ArrayList>
    					<collection:ArrayList>
    						<System:String>右の枝</System:String>
    						<CollectionContainer Collection="{Binding Right}" />
    					</collection:ArrayList>
    				</collection:ArrayList>
    

    上記の部分ですが、ArrayListはDependencyObject派生ではないので、その下でBindingはできないと思います。
    やるならRelativeSourceで参照先を指定するかResource参照するかになると思います。
    他にはCompositeCollectionを使うとか・・・ただ、やってみたけどダメでした(苦笑)

    そもそもHerarchicalDataTemplateがかなり特殊な代物のようで、ResourceにCollectionViewSourceを記述しても機能しませんでした。
    これが通ればXAMLだけでも処理できそうな気がするんですけどね。
    (先のソースで参照用プロパティを使ったりConverterを通したりしたのはそのためです)。

    以上、少しでも参考になれば幸いです。

    2012年7月30日 4:03
  • みっとさん

    追加の質問まで対応して頂き、ありがとうございます。

    私も色々試してみたのですがやはりXAMLだけで動かすことはできず、
    実際のプログラムでは、Converterを記述する方法で落ち着きました。


    挙動を観察した感じ、XAMLだけで上手く行かないのは

    ・テンプレートが適用される度に記述が解釈されて新しく要素が生成されるのは直下の要素だけ
    (上の例では <TextBlock Text="{Binding Text}"> の部分だけ )

    ・ItemsSourceなどその他の部分に関する記述は
     XAML全体の最初の読込み時に一度だけ解釈されて生成されてしまい、
     その後のテンプレート適用時にも、最初に作られたオブジェクトが使いまわされるだけ

    という実装になっているからなのかな、と思っております。

    最初に挙げたコードでは極めて挙動不審ながらも惜しい動作をしており
    行けるかと思ったのですが、CompositeCollectionのインスタンスが一つしかないので
    テンプレートが別のところで適用されると、それまでの表示が消えてしまうようです。

    仮定に基づき、テンプレート適用のたびに生成されるTextBoxのところで
    CompositeCollectionを生成して適当なプロパティに無理矢理Bindingして…
    などの方法も考えたのですが結局うまく書くことができず
    そんなトリッキーなことまでしてXAMLだけで実現しなくても良いかな、と。

    WPFはまだ不慣れなのですが、色々勉強になりました。
    ありがとうございました。

    2012年8月1日 8:22