none
ネストしたUserControl内のListBoxItemからのデータバインドについて RRS feed

  • 質問

  • 最初に、次のような構成で、ちゃんと動いている状態があります。

     

    ・MainWindowのDataContextとしてViewModel1が指定されている
    ・MainWindow内にGrid1
    ・その内部にListBox1
    ListBox1は ItemsSource={Binding 親コレクション} としてViewModel1のプロパティ(コレクション)にバインド
    ・そのDataTemplateとして、リストボックスの各行は、別のUserControl1で表示される

    ・UserControl1の中にはListBox2がある
    ・ListBox2は、 ItemsSource = "{Binding 子コレクション}" として
      ListBox1の一行に対する子コレクションにバインド

    ただしここで、ListBox2で、ViewModel1の提供する、ItemsSourceとは関係ない、独立した(コレクションではなく単一の値の)プロパティとコマンドにバインドする必要があり、ItemTemplateのDataTemplate中で次のような感じにしています。

    <TextBlock Text ="{Binding Path=DataContext.Dproperty, ElementName=Grid1}"/>
    <Button Command =" {Binding Path=DataContext.Dcommand, ElementName=Grid1}"/>
    
    

    この段階では、ListBox2に表示する値のコレクションも、それとは独立してViewModel1から提供されるプロパティとコマンドも、全て無事にバインドされ、ちゃんと動いています。

    ここで、UserControl1というのは、実は、単にListBoxが1つあるだけの単純なものではなく、複数のListBoxやそれに付随する色々な部品で構成されています。

    機能追加によって、UserControl1が肥大化したため、これをさらに、いくつかのUserControlに分けたいと思いました。

    単に、ItemsSourceを指定しているだけのListBoxとその付随要素の部分は、別のUserControlに分割してネストしても問題なく動いているのですが、上のListBox2、ItemsSourceと関係ないViewModel1のプロパティとコマンドとバインドしている部分だけが、別のUserControlに分けてネストするとうまく行きません。

    Grid1の子要素としてUserControl1を置き、その中のListBox2からはGrid1とElementBindingできるのですが、

    Grid1の子要素のUserContorl1の、そのまた子要素のUserControl2の中のListBoxという状態になるとGrid1にBindできなくなってしまうようです。

    (ListBox2でもItemsSourceの方は、ネストしても問題なくバインドできていて、それらは表示されます。)

    ネストしたUserControlの中のListBoxの中のItemから、ViewModelのプロパティやコマンドにバインドするにはどうすればよいのでしょうか?





    • 編集済み minami259 2011年12月28日 23:30
    2011年12月28日 23:21

回答

  • 実際にどのようなコードを書かれているのかわかりませんが、UserControlのElementNameにGrid1という固定された名前を書かれているのでしょうか? であれば、あまり良くないと思います。
    他に良い方法があるかもしれませんが、UserControlに親のDataContextを保存する依存関係プロパティを作成してそこにGrid1のDataContextをセットし、UserControlではそれを利用するようにしてはいかがでしょうか?

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク minami259 2012年1月4日 0:36
    2011年12月29日 9:19
    モデレータ
  • DataContextはobject型ですから、object型で作成すれば良いです。例えば、以下のようにです。

    public static readonly DependencyProperty RootPassThruDataContextProperty =
        DependencyProperty.Register("RootPassThruDataContext", typeof(object), typeof(親UserControl));
    
    public object RootPassThruDataContext
    {
        get { return (object)GetValue(RootPassThruDataContextProperty); }
        set { SetValue(RootPassThruDataContextProperty, value); }
    }
    
    

    XAMLでは、例えば以下のように指定します。

    <uc:UserControl1 RootPassThruDataContext="{Binding ElementName=Grid1, Path=DataContext}"/>
    
    

    MainWindowのListBox1の各行にあるUserControl1のDataContextには、ListBox1のそれぞれの行に対応するオブジェクトが暗黙的に設定されますので、それを書きつぶしてはいけません。

    UserControlでは以下のようにしてバインドします。

    <TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}, Path=RootPassThruDataContext.メンバー名}" />
    



     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク minami259 2012年1月4日 0:36
    2011年12月30日 8:40
    モデレータ
  • すみません、うかつでした。確かにWPFはパフォーマンスが問題になることがあります。それぞれのUserControlがFindAncestorやElementNameでバインド先を探した場合、時間がかかるかもしれませんね。ちなみに何件ぐらい表示されているのでしょうか?その件数によっては相当数のFindAncestorやElementNameが実行されてしまいます。
    考え方を変えて、ListBoxまでを含めたUserControlにしてしまい、そのUserControlのViewModelでDataContextを設定してしまうというのはいかがでしょうか? マスター詳細を1画面で構成する場合は、私はそのような構造にしています。

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク minami259 2012年1月4日 0:37
    2011年12月31日 8:01
    モデレータ

すべての返信

  • 実際にどのようなコードを書かれているのかわかりませんが、UserControlのElementNameにGrid1という固定された名前を書かれているのでしょうか? であれば、あまり良くないと思います。
    他に良い方法があるかもしれませんが、UserControlに親のDataContextを保存する依存関係プロパティを作成してそこにGrid1のDataContextをセットし、UserControlではそれを利用するようにしてはいかがでしょうか?

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク minami259 2012年1月4日 0:36
    2011年12月29日 9:19
    モデレータ
  • 返信ありがとうございます。

    親のUserControlに依存関係プロパティを作成するのは

            public static readonly DependencyProperty PassThruDataContextProperty =
                DependencyProperty.Register("PassThruDataContext", typeof((●●●)), typeof(親UserControl));
    
            public ●●● PassThruDataContext
            {
                get { return (●●●)GetValue(PassThruDataContextProperty); }
                set { SetValue(PassThruDataContextProperty, value); }
            }
    
    

    と言った感じで良いと思うのですが、DataContextを保存するためのプロパティは、●●●に入る、型の部分をどのようにすればよいのでしょうか?

    単純にdoubleとかstringとかのプロパティ1つなら、●●●にその型を入れればよいのでしょうが、ViewModelの全てのプロパティやコマンドを含め、DataContextまるまるをセットするようなプロパティの定義の方法がわかりません。

    とりあえず、ViewModelのクラス名そのものを型として●●●に入れるようにしたところ、コンパイルまではできたものの、これで良いのかわかりません。

    また、現時点では、それだと、エラーは出ないもののバインドができていません。本来のDataContextを依存関係プロパティに保存するように、

            private void 親UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
            {
                if (this.DataContext is ●●●)
                    this.RootPassThruDataContext = (●●●)this.DataContext;
            }
    

    などとして見ましたが、これで良いのでしょうか? エラーは出ていないもののバインドが機能していません。

    XAMLの方では、もともと(親UserControlのDataContextをそのまま使っていた時)は、UserControl自体を配置する個所では、

    <uc:UserControl1 />
    


    と、特にDataContextや、Sourceを指定することなく、プロパティもコマンドも全て、正常にバインドされて機能していました。

    これを依存関係プロパティの方にバインドさせるのが、どう書けばバインドしてくれるのかわかりません(そもそも依存関係プロパティの指定自体が間違っているのか、XAMLのバインドの方が間違っているのか判別できないのですが)

     

    <uc:UserControl1 DataContext="{Binding RootPathThruDataContext}">
    

    などとしてやってもバインドが全く機能しなくなります。

     

    もしよろしければ、依存関係プロパティの定義の方法や、XAMLでのバインドの方法など、具体的な

    コード例など教えて頂ければありがたいです。

    • 編集済み minami259 2011年12月29日 21:00
    2011年12月29日 20:54
  • DataContextはobject型ですから、object型で作成すれば良いです。例えば、以下のようにです。

    public static readonly DependencyProperty RootPassThruDataContextProperty =
        DependencyProperty.Register("RootPassThruDataContext", typeof(object), typeof(親UserControl));
    
    public object RootPassThruDataContext
    {
        get { return (object)GetValue(RootPassThruDataContextProperty); }
        set { SetValue(RootPassThruDataContextProperty, value); }
    }
    
    

    XAMLでは、例えば以下のように指定します。

    <uc:UserControl1 RootPassThruDataContext="{Binding ElementName=Grid1, Path=DataContext}"/>
    
    

    MainWindowのListBox1の各行にあるUserControl1のDataContextには、ListBox1のそれぞれの行に対応するオブジェクトが暗黙的に設定されますので、それを書きつぶしてはいけません。

    UserControlでは以下のようにしてバインドします。

    <TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}, Path=RootPassThruDataContext.メンバー名}" />
    



     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク minami259 2012年1月4日 0:36
    2011年12月30日 8:40
    モデレータ
  • 度々、ご回答ありがとうございます。

    なんとか、ご回答を参考に、UserControlの一部分を、別のUserControlとして切り出して、ネストして、コマンドも呼べるようにはなったのですが、ただ、新たな問題が出てきてしまいました。

    ネストしないで1つのUserControlとして実行していた時は、大元のMainWindowで、新たなデータを呼び出して、それを元に表示するまで、0.1~0.2秒というレベルの時間しかかかっていなかったのですが。

    機能・表示とも全く同じ、今回の、単に2つのUserControlに分け、DataContextを依存関係プロパティで渡すようにネストした修正をしたところ、表示されるまで5秒近くかかるようになってしまいました。

    単にコレクションにバインドしているだけのListBoxなどはすでに別のUserControlに分けていて、その際は時間に体感できるような差異は発生しませんでしたから、原因は依存関係プロパティの部分だと思います(今回のこの質問に関連した部分以外は何も変更をしていません)。

    FindAncestorでやっているのが原因かと、名前をつけてElementNameで指定したり、DataContext全体でなく、最初のバインドのところでDataContext.Commandだけを持ってくるように変えたりしてみましたが、速度の改善は見られませんでした。

    もともとデータ呼び出しから表示まで0.1~0.2秒だったものが、5秒とかかかってしまう現状だと、実用に耐えません。

    なんとか改善策はないでしょうか?

    2011年12月31日 0:21
  • すみません、うかつでした。確かにWPFはパフォーマンスが問題になることがあります。それぞれのUserControlがFindAncestorやElementNameでバインド先を探した場合、時間がかかるかもしれませんね。ちなみに何件ぐらい表示されているのでしょうか?その件数によっては相当数のFindAncestorやElementNameが実行されてしまいます。
    考え方を変えて、ListBoxまでを含めたUserControlにしてしまい、そのUserControlのViewModelでDataContextを設定してしまうというのはいかがでしょうか? マスター詳細を1画面で構成する場合は、私はそのような構造にしています。

     


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/
    • 回答としてマーク minami259 2012年1月4日 0:37
    2011年12月31日 8:01
    モデレータ
  • UserControlとViewModelの構成を見直して、バインドの方法を変えたところ、体感上、当初のものと比べて目立った速度低下がないレベルでネストできました。

    ありがとうございました。

    2012年1月4日 0:36