none
ListBoxのアイテム内に配置したButtonをクリックするとそのアイテムが選択されない RRS feed

  • 質問

  • 以下のようなListBoxで

                <ListBox x:Name="list">
                    <ListBox.ItemTemplate>
                        <ItemContainerTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
                                <Button Grid.Column="0" Content="{Binding}"/>
                                <TextBlock Grid.Column="1" Text="{Binding}"/>
                            </Grid>
                        </ItemContainerTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>

    各アイテム内に配置されているButtonをクリックしてもそのアイテムが選択されません(TextBlockをクリックすると選択されます)なぜこのような動作になってしまうのでしょうか?

    また、Buttonをクリックした際にもアイテムを選択させるにはどうしたらよいでしょうか?

    アイテムを選択させた状態で、ButtonのClickイベントで選択されているデータを処理したいと考えています。

    よろしくお願いします。

    2017年10月19日 6:19

回答

  • これで選択はされるようになったのですが
    根本的に、なぜボタンではこのような挙動になってしまうのでしょうか?

    WPFでは、入力イベントなどにルーティングイベントの概念を導入しています。ある座標におけるカーソル操作は、まず一番奥に存在するWindowに通知され、だんだん手前に上ってきて、最終的に一番手前(=ユーザが直接目にする)要素に通知されます。ここまでをトンネリングと言います。さらにこの後、逆方向にも順次通知が行われます。こちらはバブリングと言います。

    マウスの左ボタン押下を例にとると、トンネリングで通知されるイベントはPreviewMouseLeftButtonDown、バブリングで通知されるイベントはMouseLeftButtonDownになります。Window上にGridを配置し、Grid上にLabelを配置して、このLabel上でマウスの左ボタンを押下すると、Window - Grid - Label の順にPreviewMouseLeftButtonDownが発生し、その後Label - Grid - Windowの順にMouseLeftButtonDownが発生します。

    ルーティングイベントでは、"処理済み"という機能が存在しています(RoutedEventArgs.Handled)。これは、イベント中にマークすることで、それ以降の通知を停止するものです。

    分かりやすい例は、入れ子になったScrollViewer上でマウスホイールを動かしたケースでしょうか。この場合、子のScrollViewrerはスクロールし、親のScrollViewrerはスクロールしない、という動作を期待すると思います。なので、子のScrollViewerはMouseWheelイベントにてHandled=trueに設定し、バブリングで通知される親のScrollViewrerがMouseWheelイベントを処理しないようにします。

    今回の場合、Buttonがクリックへの対応のためにマウス系のイベントをHandled=trueにしたので、親となるListBoxItemがマウスイベントを処理できなかった、ということですね。

    • 回答としてマーク ikarimame 2017年10月20日 2:10
    2017年10月20日 1:00

すべての返信

  • こんにちは。

    以下のリンク先の方法を使ってClickイベント時に前もってコードビハインドで選択させれば、
    ListBoxItemを選択状態にしたうえで、クリック処理が出来ます。

    https://stackoverflow.com/questions/3720941/how-to-select-listboxitem-upon-clicking-on-button-in-template

    2017年10月19日 6:44
    モデレータ
  • ありがとうございます、教えていただいたサイトを参考に以下のように実装しました。
    (残念ながら私の知識不足によりAssociatedObjectというものの使い方を理解するまでは
    至りませんでしたのでVisualTreeHelperを使用してListBoxItemを特定しています。)

        public class MyBehavior
        {
            public static bool GetIsClickToSelect(DependencyObject obj)
            {
                return (bool)obj.GetValue(IsClickToSelectProperty);
            }
            public static void SetIsClickToSelect(DependencyObject obj, bool value)
            {
                obj.SetValue(IsClickToSelectProperty, value);
            }
            // Using a DependencyProperty as the backing store for IsClickToSelect.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty IsClickToSelectProperty =
                DependencyProperty.RegisterAttached("IsClickToSelect", typeof(bool), typeof(MyBehavior), new PropertyMetadata(false, OnIsClickToSelectChanged));
            private static void OnIsClickToSelectChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                if (GetListBoxItem(d) == null) throw new NotSupportedException();
                var btn = d as Button;
                if (btn == null) throw new NotSupportedException();
                var val = (bool)e.NewValue;
                if (val) btn.Click += Btn_Click;
                else btn.Click -= Btn_Click;
            }
            private static void Btn_Click(object sender, RoutedEventArgs e)
            {
                object clicked = (e.OriginalSource as FrameworkElement).DataContext;
                var btn = (Button)e.OriginalSource;
                var lbi = GetListBoxItem(btn);
                if (lbi != null) lbi.IsSelected = true;
                //var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromItem(clicked) as ListBoxItem;
                //lbi.IsSelected = true;
            }
            private static ListBoxItem GetListBoxItem(DependencyObject d)
            {
                var ret = VisualTreeHelper.GetParent(d);
                while (ret != null && !(ret is ListBoxItem))
                {
                    ret = VisualTreeHelper.GetParent(ret);
                }
                return ret as ListBoxItem;
            }
        }
                <ListBox x:Name="list">
                    <ListBox.ItemTemplate>
                        <ItemContainerTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
                                <Button Grid.Column="0" Content="{Binding}" local:MyBehavior.IsClickToSelect="True"/>
                                <TextBlock Grid.Column="1" Text="{Binding}"/>
                            </Grid>
                        </ItemContainerTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>

    これで選択はされるようになったのですが
    根本的に、なぜボタンではこのような挙動になってしまうのでしょうか?


    • 編集済み ikarimame 2017年10月19日 9:23 コードが間違っていた
    2017年10月19日 7:50
  • これで選択はされるようになったのですが
    根本的に、なぜボタンではこのような挙動になってしまうのでしょうか?

    WPFでは、入力イベントなどにルーティングイベントの概念を導入しています。ある座標におけるカーソル操作は、まず一番奥に存在するWindowに通知され、だんだん手前に上ってきて、最終的に一番手前(=ユーザが直接目にする)要素に通知されます。ここまでをトンネリングと言います。さらにこの後、逆方向にも順次通知が行われます。こちらはバブリングと言います。

    マウスの左ボタン押下を例にとると、トンネリングで通知されるイベントはPreviewMouseLeftButtonDown、バブリングで通知されるイベントはMouseLeftButtonDownになります。Window上にGridを配置し、Grid上にLabelを配置して、このLabel上でマウスの左ボタンを押下すると、Window - Grid - Label の順にPreviewMouseLeftButtonDownが発生し、その後Label - Grid - Windowの順にMouseLeftButtonDownが発生します。

    ルーティングイベントでは、"処理済み"という機能が存在しています(RoutedEventArgs.Handled)。これは、イベント中にマークすることで、それ以降の通知を停止するものです。

    分かりやすい例は、入れ子になったScrollViewer上でマウスホイールを動かしたケースでしょうか。この場合、子のScrollViewrerはスクロールし、親のScrollViewrerはスクロールしない、という動作を期待すると思います。なので、子のScrollViewerはMouseWheelイベントにてHandled=trueに設定し、バブリングで通知される親のScrollViewrerがMouseWheelイベントを処理しないようにします。

    今回の場合、Buttonがクリックへの対応のためにマウス系のイベントをHandled=trueにしたので、親となるListBoxItemがマウスイベントを処理できなかった、ということですね。

    • 回答としてマーク ikarimame 2017年10月20日 2:10
    2017年10月20日 1:00
  • AssociatedObjectは、その名の通り、Behaviorに結びついているオブジェクトのことです。BehaviorはXAMLにロジックを与える仕組みのようなものです。XAML自体、オブジェクトを生成する役割を持ちますが、その生成されたオブジェクトにロジックを付加したい場合に、Behaviorを用います。なので、AssociatedObjectは、Behaviorと結びついているXAMLにより生成されたオブジェクトになります。
    サンプルが載っているページでは、ListBoxに対してBehaviorを結びつけていますから、AssociatedObjectはListBoxになります。このように、AssociatedObjectはどのXAML(XAMLより生成されたオブジェクト)に結びついているかによって、変わります。

    また、ボタンをクリックしても選択されないのは、Hongliangさんが書かれた通りで、Clickイベントが拾えないからです。しかし、PreviewMouseLeftButtonDownイベントは拾えますので、

    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <EventSetter Event="PreviewMouseLeftButtonDown" Handler="ListBoxItem_PreviewMouseLeftButtonDown" />
        </Style>
    </ListBox.ItemContainerStyle>

    としておいて、コードビハインドで、

    private void ListBoxItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        var item = sender as ListBoxItem;
        item.IsSelected = true;
    }
    とすることもできます。このことから、ルーティングイベントについて理解がより深まると思います。


    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/


    • 編集済み trapemiyaModerator 2017年10月20日 1:44 説明をよりわかりやすく編集
    2017年10月20日 1:40
    モデレータ
  • ありがとうございます、教えていただき下記を確認したところ

    http://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/Primitives/ButtonBase.cs,414

    確かにHandled=trueにしていることが確認できました、試しにClickModeをHoverにしてみたところListItemが選択されました。

    2017年10月20日 2:10