locked
再帰的な構造をもつテーブルを TreeView で表示させるには? RRS feed

  • 質問

  • 以下を参考に、
    階層構造をもつデータを TreeView で表示させることが出来ました。
     
     
     
    図1:階層構造をもつテーブルを TreeView で表示
     
     
    但し、データはコードで以下のように生成し、TreeView にバインドしています。
     
    public partial class MainPage : UserControl
    {
       static public ObservableCollection<Topic> Topics = new ObservableCollection<Topic>();
       public MainPage()
       {
          InitializeComponent();
    
          Topics.Add(new Topic("A", -1));
          Topics.Add(new Topic("B", 1));
    
          Topic DataGridTopic = new Topic("C", 4);
          DataGridTopic.ChildTopics.Add(new Topic("C.0", -1));
          DataGridTopic.ChildTopics.Add(new Topic("C.1", -1));
          DataGridTopic.ChildTopics.Add(new Topic("C.2", 1));
          Topics.Add(DataGridTopic);
    
          Topics.Add(new Topic("D", 1));
          treeView1.DataContext = Topics;
       }
    }
    

    しかし、
    こんな風にデータを手作業で生成させることなんて殆ど無いでしょう。
    実際にはデータベースのデータが使われる筈。
     
    そこで、
    再帰的な構造をもつテーブルのデータを TreeView に表示させたいと思いますが、
    仮に以下のようなテーブルとデータを用意したとします。
     
     
     
    図2:再帰的な構造をもつテーブル
     
     
    このような場合、そもそも、
    データをどのように取得し、
    どう処理して TreeView に適用させれば良いのか、現状見当が付きません。
     
     
    もしかして
    下記ドキュメントの「再帰クエリ」を使うのか、
    使うとしても Silverlight でどう使えば良いのか、全く分かりません。
     
     
     
    ご存じの方いらっしゃいませんか?







    • 編集済み custar 2011年12月29日 14:59
    2011年12月29日 11:17

回答

  • どこかで再帰する関数を書く必要がありますね。
    たとえば、今回の例だと、

    /// <summary>
    /// Modelクラス(データベースから取ってきたデータの入れ物のクラス)
    /// </summary>
    public class TopicModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int? ParentId { get; set; }
    }
    
    /// <summary>
    /// TreeViewにバインドするためクラス
    /// </summary>
    public class TopicViewModel
    {
        public string Title { get; set; }
        public ObservableCollection<TopicViewModel> Child { get; set; }
    
        public TopicViewModel(string title, IEnumerable<TopicViewModel> child)
        {
            Title = title;
            Child = new ObservableCollection<TopicViewModel>(child);
        }
    }
    
    //上のようなクラスをつくっておいて・・・
    
    //データベースから取ってきたデータが以下だったとする
    var dataBaseData = new List<TopicModel>
    {
        new TopicModel { Id = 1, Name = "b.0", ParentId = null },
        new TopicModel { Id = 2, Name = "b.1", ParentId = 1 },
        new TopicModel { Id = 3, Name = "b.2", ParentId = 1 },
        new TopicModel { Id = 4, Name = "b.3", ParentId = null },
        new TopicModel { Id = 5, Name = "b.4", ParentId = 4 },
        new TopicModel { Id = 6, Name = "b.5", ParentId = 4 },
        new TopicModel { Id = 7, Name = "b.6", ParentId = null },
        new TopicModel { Id = 8, Name = "b.7", ParentId = 5 },
        new TopicModel { Id = 9, Name = "b.8", ParentId = 5 },
        new TopicModel { Id = 10, Name = "b.9", ParentId = 6 },
    };
    
    //再帰的に呼び出す関数を定義する
    Func<TopicModel, IEnumerable<TopicModel>, TopicViewModel> f = null;
    f = (model, models) =>
                new TopicViewModel(
                    model.Name,
                    models
                    .Where(m => m.ParentId == model.Id)
                    .Select(m => f(m, models))
                );
    
    //上の関数を使ってデータバインド用階層データを作る
    Items = new ObservableCollection<TopicViewModel>(
        dataBaseData
            .Where(m => m.ParentId == null)
            .Select(m => f(m, dataBaseData)));
            
    


           
    と、こんな感じになりますかね。


    ※ MSDNのXAMLの例だと2階層までしか表現できないので、全階層表現するためには”NameTemplate”を改造する必要がありますけど、そこは本筋でないのと、たぶんすぐわかると思うので割愛します
    ※ TopicModelクラスのNotifyPropertyChange等は本筋ではないので省略してますので、OneTimeバインディングになります。
    • 回答としてマーク 山本春海 2012年1月27日 8:16
    2011年12月29日 18:12

すべての返信

  • どこかで再帰する関数を書く必要がありますね。
    たとえば、今回の例だと、

    /// <summary>
    /// Modelクラス(データベースから取ってきたデータの入れ物のクラス)
    /// </summary>
    public class TopicModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int? ParentId { get; set; }
    }
    
    /// <summary>
    /// TreeViewにバインドするためクラス
    /// </summary>
    public class TopicViewModel
    {
        public string Title { get; set; }
        public ObservableCollection<TopicViewModel> Child { get; set; }
    
        public TopicViewModel(string title, IEnumerable<TopicViewModel> child)
        {
            Title = title;
            Child = new ObservableCollection<TopicViewModel>(child);
        }
    }
    
    //上のようなクラスをつくっておいて・・・
    
    //データベースから取ってきたデータが以下だったとする
    var dataBaseData = new List<TopicModel>
    {
        new TopicModel { Id = 1, Name = "b.0", ParentId = null },
        new TopicModel { Id = 2, Name = "b.1", ParentId = 1 },
        new TopicModel { Id = 3, Name = "b.2", ParentId = 1 },
        new TopicModel { Id = 4, Name = "b.3", ParentId = null },
        new TopicModel { Id = 5, Name = "b.4", ParentId = 4 },
        new TopicModel { Id = 6, Name = "b.5", ParentId = 4 },
        new TopicModel { Id = 7, Name = "b.6", ParentId = null },
        new TopicModel { Id = 8, Name = "b.7", ParentId = 5 },
        new TopicModel { Id = 9, Name = "b.8", ParentId = 5 },
        new TopicModel { Id = 10, Name = "b.9", ParentId = 6 },
    };
    
    //再帰的に呼び出す関数を定義する
    Func<TopicModel, IEnumerable<TopicModel>, TopicViewModel> f = null;
    f = (model, models) =>
                new TopicViewModel(
                    model.Name,
                    models
                    .Where(m => m.ParentId == model.Id)
                    .Select(m => f(m, models))
                );
    
    //上の関数を使ってデータバインド用階層データを作る
    Items = new ObservableCollection<TopicViewModel>(
        dataBaseData
            .Where(m => m.ParentId == null)
            .Select(m => f(m, dataBaseData)));
            
    


           
    と、こんな感じになりますかね。


    ※ MSDNのXAMLの例だと2階層までしか表現できないので、全階層表現するためには”NameTemplate”を改造する必要がありますけど、そこは本筋でないのと、たぶんすぐわかると思うので割愛します
    ※ TopicModelクラスのNotifyPropertyChange等は本筋ではないので省略してますので、OneTimeバインディングになります。
    • 回答としてマーク 山本春海 2012年1月27日 8:16
    2011年12月29日 18:12
  • Uchida さん、情報有り難うございます。

    書かれている内容で分からない部分が幾つかありますので、
    まずはその点を調べてみます。
    • 編集済み custar 2011年12月30日 5:12
    2011年12月30日 5:11
  • ※ MSDNのXAMLの例だと2階層までしか表現できない
    確かに。
    全階層表現するためには”NameTemplate”を改造する必要があります
    xaml 学習中なので Uchida さんの考えられている方法とは違うと思いますが、
    以下の記述で、図のような表現が出来ました。
     
     
    <Grid x:Name="LayoutRoot" Background="White">
       <Grid.Resources>
          <sdk:HierarchicalDataTemplate x:Key="ChildTemplate" 
                            ItemsSource="{Binding Path=ChildTopics}">
             <TextBlock Text="{Binding Path=Title}" />
          </sdk:HierarchicalDataTemplate>
             
          <sdk:HierarchicalDataTemplate x:Key="NameTemplate" 
                            ItemsSource="{Binding Path=ChildTopics}" 
                            ItemTemplate="{StaticResource ChildTemplate}">
             <TextBlock Text="{Binding Path=Title}" />
          </sdk:HierarchicalDataTemplate>
       </Grid.Resources>
          
       <sdk:TreeView ItemsSource="{Binding}" 
                ItemTemplate="{StaticResource NameTemplate}" ... />
    </Grid>
    
     
     
    ChildTemplate にも「ItemsSource="{Binding Path=ChildTopics}"」を付けてみました。
    上記 xaml は簡略化できそうな気もしますが。
     
     
    public partial class MainPage : UserControl
    {
       static public ObservableCollection<Topic> Topics = new ObservableCollection<Topic>();
       public MainPage()
       {
          InitializeComponent();
    
          Topic 
             b0 = new Topic("b.0"),
             b1 = new Topic("b.1"),
             b2 = new Topic("b.2"),
             b3 = new Topic("b.3"),
             b4 = new Topic("b.4"),
             b5 = new Topic("b.5"),
             b6 = new Topic("b.6"),
             b7 = new Topic("b.7");
    
          b0.ChildTopics.Add(b1);
          b1.ChildTopics.Add(b2);
          b2.ChildTopics.Add(b3);
          b3.ChildTopics.Add(b4);
          b4.ChildTopics.Add(b5);
          b5.ChildTopics.Add(b6);
          b6.ChildTopics.Add(b7);
    
          Topics.Add(b0);
          treeView1.DataContext = Topics;
       }
    }


    • 編集済み custar 2011年12月31日 9:04
    2011年12月31日 8:16
  • //データベースから取ってきたデータが以下だったとする
    var dataBaseData = new List<TopicModel>
    {
        new TopicModel { Id = 1, Name = "b.0", ParentId = null },
        new TopicModel { Id = 2, Name = "b.1", ParentId = 1 },
    ...(中略)...
    };      
    
     
    DataSources に以下のような b を準備できている状態で、
    上記 List<T> を用意するにはどうしたらいいのでしょう?
     
     
     
     
     
     
     
     
    上記 b を TreeView にして、
    UserControl 上にドラッグドロップすると、
    以下が MainPage.xaml.cs に挿入されます。
     
     
    public partial class MainPage : UserControl
    {
       private void bDomainDataSource_LoadedData(object sender, Controls.LoadedDataEventArgs e)
       {
          if (e.HasError)
          {
             MessageBox.Show(e.Error.ToString(), "Load Error", MessageBoxButton.OK);
             e.MarkErrorAsHandled();
          }
       }
    
     
     
    MainPage.xaml には以下のように出力されます。
     
     
    <riaControls:DomainDataSource AutoLoad="True" 
                      d:DesignData="{d:DesignInstance my:b, CreateList=true}" 
                      LoadedData="bDomainDataSource_LoadedData" 
                      Name="bDomainDataSource" 
                      QueryName="GetBsQuery"  ...>
       <riaControls:DomainDataSource.DomainContext>
          <my:DomainService1 />
       </riaControls:DomainDataSource.DomainContext>
    </riaControls:DomainDataSource>
    
    <sdk:TreeView Name="bTreeView" 
             ItemsSource="{Binding ElementName=bDomainDataSource, Path=Data}" .../>
    
     
     
    何かしら記述できそうな気がしますが、どう書くべきなのか....


    • 編集済み custar 2011年12月31日 9:58
    2011年12月31日 9:28
  • もう解決されているかもですが、
    DataSourceのTreeViewテンプレートはDataGrid等と違って細かく設定してくれません。
    ですので、上の方で書かれたXamlをそのまま書いてあげる必要があります。

    また、DomainDataSourceコントロールを使用する場合は、DomainServiceから取得したデータがすでに階層構造になっている必要があります。
    つまり、前に私が「どこかで再帰する関数を書く必要がある」と言った「どこか」はDomainService側、もしくはもっと奥になります。
    2012年1月4日 16:39
  • DomainDataSourceコントロールを使用する場合は、
    DomainServiceから取得したデータがすでに階層構造になっている必要があります。

    つまり、
    前に私が「どこかで再帰する関数を書く必要がある」と言った「どこか」は
    DomainService側、もしくはもっと奥になります。
    なるほど。
     
    確かに silverlight 外の開発ではそうしていますね。
    2012年1月4日 17:01
  • 言葉足らずの面があったので補足します。
    「DomainDataSourceコントロールを使用する場合は」というのは、
    「WCF RIA Services + Entity Frameworkを使用する場合は」に置き換えてください。

    その上で、WCF RIA Services + Entity Frameworkを使用して階層構造データを取得し、
    TreeViewに表示させる場合は、
    Modelが再帰構造になっていれば再帰関数を書く必要もありません。

    下記に例を示します。

    # コードファースト前提ですが、データベースファーストでも同様です。
    # 環境はSilverlight5, WCF RIA Services V1 SP2, Entity Framework4.1, SQL Server Compact4.0です。

    ■データ定義
        /// <summary>
        /// コードファーストで定義するテーブル
        /// </summary>
        public class TestTable
        {
            [Key]
            public int Id { get; set; }
            public string Name { get; set; }
    
            //TestTable.IDを参照する外部キー
            public int? TestTableId { get; set; }
    
            //ナビゲーションプロパティ
            public ICollection<TestTable> Children { get; set;  }
        }
    


    ■初期データ

     

     public class TestDbInizializer : DropCreateDatabaseIfModelChanges<TestDbContext>
        {
            protected override void Seed(TestDbContext context)
            {
                var table = new List<TestTable> {
    
                    new TestTable { Id = 1, Name = "1" },
                    new TestTable { Id = 2, Name = "2" },
                    new TestTable { Id = 3, Name = "3", TestTableId = 1 },
                    new TestTable { Id = 4, Name = "4", TestTableId = 3 },
                    new TestTable { Id = 5, Name = "5", TestTableId = 2 },
                    new TestTable { Id = 6, Name = "6", TestTableId = 1 },
                    new TestTable { Id = 7, Name = "7", TestTableId = 1 },
                    new TestTable { Id = 8, Name = "8", TestTableId = 4 },
                    new TestTable { Id = 9, Name = "9", TestTableId = 8 },
                };
    
                table.ForEach(b => context.TestTables.Add(b));
                context.SaveChanges();   
            }
        }
    

     

    ■Xaml

     

        <UserControl.Resources>
            <sdk:HierarchicalDataTemplate x:Key="ChildTemplate" ItemsSource="{Binding Children}">
                <TextBlock FontStyle="Italic" Text="{Binding Name}" />
            </sdk:HierarchicalDataTemplate>
            
            <sdk:HierarchicalDataTemplate x:Key="HierTemplate" 
                ItemsSource="{Binding Children}" 
                ItemTemplate="{StaticResource ChildTemplate}">
                <TextBlock Text="{Binding Name}" FontWeight="Bold" />
            </sdk:HierarchicalDataTemplate>
        </UserControl.Resources>
        
        <Grid x:Name="LayoutRoot" Background="White">      
            <riaControls:DomainDataSource AutoLoad="True" Height="0" 
                                          LoadedData="testTableDomainDataSource_LoadedData" 
                                          x:Name="testTableDomainDataSource" 
                                          QueryName="GetTestTablesQuery" Width="0">
                <riaControls:DomainDataSource.DomainContext>
                    <my1:TestDomainContext />
                </riaControls:DomainDataSource.DomainContext>
            </riaControls:DomainDataSource>
            
            <sdk:TreeView Height="200" HorizontalAlignment="Left"                
                Margin="85,25,0,0" x:Name="testTableTreeView" VerticalAlignment="Top" Width="120"
                ItemTemplate="{StaticResource HierTemplate}"/>
        </Grid>
    

     


    ■Xaml.cs

     

            private void testTableDomainDataSource_LoadedData(object sender, LoadedDataEventArgs e)
            {
                if (e.HasError)
                {
                    System.Windows.MessageBox.Show(e.Error.ToString(), "Load Error", System.Windows.MessageBoxButton.OK);
                    e.MarkErrorAsHandled();
                }
                else
                {
                    testTableTreeView.ItemsSource = e.Entities.Cast<TestTable>().Where(x => !x.TestTableId.HasValue);
                }         
            }
    

     

     

    以上でTree表示されます。

    僕自身はWCF RIA Servicesを使うときにDomainDataSouceコントロールはあまり使いません。MVVMとの相性が悪いというか、使い勝手がよくないので普段はViewModelから操作するようにしています。
    上記はあくまで「こういうことができる」という参考程度にしてください。

    定石とか探すのも大事ですが、細部まで定石があるわけではないのでまずは自分でいろいろやってみるといいと思います。

     


    ☆TFC Software http://www.tfc-software.com/
    ☆プログラミングに関するブログ http://www.tfc-software.com/Blogs.aspx
    2012年1月8日 4:48
  • Uchida さん、大変有難う御座います。

    MVVM のドキュメントを読み漁っているところです (日本語限定)。
    読み終わったら、再度 Uchida さんのコードを見直します。

    ありがとう。

    2012年1月8日 18:11