LoginSignup
18
13

More than 5 years have passed since last update.

[WPF] TextBoxを含んだListBoxItemでのIsSelectedにまつわる悩みと解消法

Last updated at Posted at 2015-06-11

解消法なんてタイトルに付けましたが、世界の諸先輩方の知恵を借りて解決しただけのことです。今後のための備忘録としてまとめてみます。まだ動作確認も甘いので利用される方は自己責任ということでご了承ください。

概要

皆さんもTextBoxを含んだListBoxItemでTextBoxにFocusされたらListBoxItemのIsSelectedプロパティもTrueにしたいって事ありますよね。私もそうです。ネットで調べれば引っかかるのがListBoxItemのStyle.TriggersでIsKeyboardFocusWithinがTrueならばIsSelectedもTrueにするという方法です。こんな感じです。

~略~
<Style>
    <Style.Trigger>
        <Trigger Property="IsKeyboardFocusWithin" Value="True">
          <Setter Property="IsSelected" Value="True"/>
        </Trigger>
    </Style.Trigger>
</Style>

確かにこれでTextBoxにFocusが来た時に親のListBoxItemも選択されるようになりました。しかし、これにはちょっとした罠がありました…。

サンプルアプリケーションを用意

試しに動かします。検証した環境は以下の通りです。

  • Windows7 64bit
  • VisualStudio 2013
  • .NET Framework 4.5
  • Livet 1.1.0.0

プロジェクトの新規作成でLivet WPF4.5 MVVMアプリケーションを選択します。プロジェクトが作成されたら、今回検証で使うデータを用意していきます。

Person.cs
using System;
using Livet;

namespace Sample.Models
{
    public class Person : NotificationObject
    {
        #region Name変更通知プロパティ
        private string _Name;

        public string Name
        {
            get
            { return _Name; }
            set
            { 
                if (_Name == value)
                    return;
                _Name = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        public Person()
        {
            _Name = String.Empty;
        }
        public Person(Person src)
        {
            _Name = src.Name;
        }
    }
}

Nameというプロパティを持つだけのクラスです。次にNationというクラスを作成します。

Nation.cs
using System;
using Livet;

namespace Sample.Models
{
    public class Nation : NotificationObject
    {

        #region Name変更通知プロパティ
        private string _Name;

        public string Name
        {
            get
            { return _Name; }
            set
            { 
                if (_Name == value)
                    return;
                _Name = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region Members変更通知プロパティ
        private ObservableSynchronizedCollection<Person> _Members;

        public ObservableSynchronizedCollection<Person> Members
        {
            get
            { return _Members; }
            set
            { 
                if (_Members == value)
                    return;
                _Members = value;
                RaisePropertyChanged();
            }
        }
        #endregion
        public Nation()
        {
            _Name = String.Empty;
            _Members = new ObservableSynchronizedCollection<Person>();
        }
        public Nation(Nation src)
        {
            _Name = src.Name;
            _Members = new ObservableSynchronizedCollection<Person>(src.Members);
        }
    }
}

こちらは先程定義したPersonクラスのコレクションとNameというプロパティを持つだけのクラスです。これらを使いListBoxにデータバインディングしていきます。MainWindowViewModel.csで以下のようにします。

MainWindowViewModel.cs
namespace Sample.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        #region Nations変更通知プロパティ
        private ObservableSynchronizedCollection<Nation> _Nations;

        public ObservableSynchronizedCollection<Nation> Nations
        {
            get
            { return _Nations; }
            set
            { 
                if (_Nations == value)
                    return;
                _Nations = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region SelectedNation変更通知プロパティ
        private Nation _SelectedNation;

        public Nation SelectedNation
        {
            get
            { return _SelectedNation; }
            set
            { 
                if (_SelectedNation == value)
                    return;
                _SelectedNation = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region SelectedPerson変更通知プロパティ
        private Person _SelectedPerson;

        public Person SelectedPerson
        {
            get
            { return _SelectedPerson; }
            set
            { 
                if (_SelectedPerson == value)
                    return;
                _SelectedPerson = value;
                RaisePropertyChanged();
            }
        }
        #endregion
        public void Initialize()
        {

            ObservableSynchronizedCollection<Person> members = new ObservableSynchronizedCollection<Person>();
            members.Add(new Person(){
                Name = "曹操",
            });
            members.Add(new Person(){
                Name = "夏侯惇",
            });
            members.Add(new Person(){
                Name = "夏侯淵",
            });
            members.Add(new Person()
            {
                Name = "曹仁",
            });
            Nation nation = new Nation();
            nation.Name = "魏";
            nation.Members = new ObservableSynchronizedCollection<Person>(members);
            Nations.Add(nation);

            members = new ObservableSynchronizedCollection<Person>();
            members.Add(new Person()
            {
                Name = "孫堅",
            });
            members.Add(new Person()
            {
                Name = "黄蓋",
            });
            members.Add(new Person()
            {
                Name = "程普",
            });
            members.Add(new Person()
            {
                Name = "韓当",
            });
            nation = new Nation();
            nation.Name = "呉";
            nation.Members = new ObservableSynchronizedCollection<Person>(members);
            Nations.Add(nation);

            members = new ObservableSynchronizedCollection<Person>();
            members.Add(new Person()
            {
                Name = "劉備",
            });
            members.Add(new Person()
            {
                Name = "関羽",
            });
            members.Add(new Person()
            {
                Name = "張飛",
            });
            members.Add(new Person()
            {
                Name = "趙雲",
            });
            nation = new Nation();
            nation.Name = "蜀";
            nation.Members = new ObservableSynchronizedCollection<Person>(members);
            Nations.Add(nation);
        }

        public MainWindowViewModel()
        {
            _Nations = new ObservableSynchronizedCollection<Nation>();
        }
    }
}

Nationクラスのコレクションと選択されたNation、Personをバインディングさせるプロパティを用意します。そしてInitializeで適当なデータを作成します。次はXAMLをいじります。インターフェースとしてはWindowに2つのListBoxを載せます。左にNationの一覧、右に選択されたNationの持つMembersの一覧が出るようにします。

MainWindow.xaml

~省略~
  <Window.Resources>
    <Style x:Key="editableListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
      <Setter Property="OverridesDefaultStyle" Value="True" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type ContentControl}">
            <Border Background="{TemplateBinding Background}">
              <ContentPresenter />
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>

      <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="Background" Value="LightBlue" />
        </Trigger>
        <Trigger Property="IsMouseOver" Value="False">
          <Setter Property="Background" Value="LightGray" />
        </Trigger>
        <Trigger Property="IsSelected" Value="True">
          <Setter Property="Background" Value="Plum" />
        </Trigger>
      </Style.Triggers>

    </Style>
  </Window.Resources>

~省略~

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <!--Nationリスト-->
    <ListBox
      Grid.Column="0"
      Name="lbNation"
      Margin="2, 2, 4, 2"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch"
      ItemsSource="{Binding Path=Nations}"
      SelectedItem="{Binding Path=SelectedNation}"
      ItemContainerStyle="{StaticResource editableListBoxItemStyle}">
      <ItemsControl.Template>
        <ControlTemplate TargetType="ItemsControl">
          <Border 
            BorderThickness="1"
            BorderBrush="Black"
            Background="White">
            <ScrollViewer 
              HorizontalScrollBarVisibility="Auto"
              VerticalScrollBarVisibility="Auto"
              Margin="1">
              <ItemsPresenter Margin="10"/>
            </ScrollViewer>
          </Border>
        </ControlTemplate>
      </ItemsControl.Template>

      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <StackPanel Orientation="Vertical" />
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>

      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <Grid HorizontalAlignment="Stretch">
            <TextBox 
              Margin="4" 
              Text="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
          </Grid>
        </DataTemplate>
      </ItemsControl.ItemTemplate>

    </ListBox>

    <!--Memberリスト-->
    <ListBox
      Grid.Column="1"
      Name="lbPerson"
      Margin="2, 2, 4, 2"
      HorizontalAlignment="Stretch"
      VerticalAlignment="Stretch"
      ItemsSource="{Binding Path=SelectedNation.Members}"
      SelectedItem="{Binding Path=SelectedPerson}"
      ItemContainerStyle="{StaticResource editableListBoxItemStyle}">
      <ItemsControl.Template>
        <ControlTemplate TargetType="ItemsControl">
          <Border 
            BorderThickness="1"
            BorderBrush="Black"
            Background="White">
            <ScrollViewer 
              HorizontalScrollBarVisibility="Auto"
              VerticalScrollBarVisibility="Auto"
              Margin="1">
              <ItemsPresenter Margin="10"/>
            </ScrollViewer>
          </Border>
        </ControlTemplate>
      </ItemsControl.Template>

      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <StackPanel Orientation="Vertical" />
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>

      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <Grid HorizontalAlignment="Stretch">
            <TextBox 
              Margin="4" 
              Text="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
          </Grid>
        </DataTemplate>

      </ItemsControl.ItemTemplate>

    </ListBox>
  </Grid>
~省略~

長いので一部省略してます。説明用にListBoxItemのスタイルをリソースとして切り出しています。このスタイル定義ではListBoxItemは選択時には紫、マウスオーバー時は水色、それ以外はグレーとなります。この状態でビルドして実行するとこんな感じにNationの一覧が表示されます。
初期画面

動作確認

Nationを選択するとこんな感じ
Nation選択

右のListBoxにMemberが展開されます。そして、Memberを選択するとこんな感じ。
Member選択

そしておもむろに左のListBox内のTextBoxを編集します。
TextBox編集1

TextBoxにフォーカスは移りましたが、ListBoxItemが選択されません。ここまではTextBox in ListBoxItemの標準動作です。

実験1 ListBoxItemのStyle.TriggersにIsKeyboardFocusWithinを追加してみる

これを解決するために冒頭で載せたスタイル定義を追加します。

MainWindow.xaml
~省略~
  <Window.Resources>
    <Style x:Key="editableListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
      <Setter Property="OverridesDefaultStyle" Value="True" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type ContentControl}">
            <Border Background="{TemplateBinding Background}">
              <ContentPresenter />
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>

      <Style.Triggers>
        <!--追加!-->
        <Trigger Property="IsKeyboardFocusWithin" Value="True">
          <Setter Property="IsSelected" Value="True"/>
        </Trigger>

        <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="Background" Value="LightBlue" />
        </Trigger>
        <Trigger Property="IsMouseOver" Value="False">
          <Setter Property="Background" Value="LightGray" />
        </Trigger>
        <Trigger Property="IsSelected" Value="True">
          <Setter Property="Background" Value="Plum" />
        </Trigger>
      </Style.Triggers>

    </Style>
  </Window.Resources>
~省略~

実行してみましよう。
Trigger追加

今度はTexBoxのフォーカスと連動してListBoxItemが選択されるようになりました。右のMemberリストも更新されています。ではMemberのリストを選択してみます。関羽を選択してみます。
初期画面

おわかりいただけたでしょうか。「催眠術だとか超スピードだとかそんなチャチなもんじゃあ断じてねえ」事が起きました。別のコントロールにフォーカスが移ると、ListBoxのSelectedItemのバインディングがnullになってしまいます。最初の動作確認を振り返ってください。IsKeyboardFocusWithinのトリガーを実装する前はたとえフォーカスが外れても選択情報は維持されたままでした。このケースにおいてはIsKeyboardFocusWithinを利用したフラグ設定では不十分なようです。

実験2 よろしい、ならばBehaviorだ

困った時はstackoverflowです。調べてみると同じ問題にぶつかった先輩が居ました。幾つかソリューションが提案されていますが、最後のBehaviorを実装する方法が他のコントロールにも流用できそうなので採用してみます。この先はstackoverflowに書かれている内容と同じです。先ほどのページに書かれている通りにクラスを作成します。

AutoSelectWhenAnyChildGetsFocus.cs

using System.Windows;
using System.Windows.Controls.Primitives;

namespace Sample.Behaviors
{
    public class AutoSelectWhenAnyChildGetsFocus
    {
        public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached(
            "Enabled",
            typeof(bool),
            typeof(AutoSelectWhenAnyChildGetsFocus),
            new UIPropertyMetadata(false, Enabled_Changed));

        public static bool GetEnabled( DependencyObject obj ) { return (bool)obj.GetValue(EnabledProperty); }
        public static void SetEnabled( DependencyObject obj, bool value ) { obj.SetValue(EnabledProperty, value); }

        private static void Enabled_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var attachEvents = (bool)e.NewValue;
            var targetUiElement = (UIElement)sender;

            if (attachEvents)
            {
                targetUiElement.IsKeyboardFocusWithinChanged += TargetUiElement_IsKeyboardFocusWithinChanged;
            }
            else
            {
                targetUiElement.IsKeyboardFocusWithinChanged -= TargetUiElement_IsKeyboardFocusWithinChanged;
            }
        }

        static void TargetUiElement_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var targetUiElement = (UIElement)sender;

            if(targetUiElement.IsKeyboardFocusWithin)
            {
                Selector.SetIsSelected(targetUiElement, true);
            }
        }
    }
}

そして、ListBoxItemのStyleを以下のように変更します。

  <Style x:Key="editableListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="OverridesDefaultStyle" Value="True" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type ContentControl}">
            <Border Background="{TemplateBinding Background}">
              <ContentPresenter />
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
      <!--ビヘイビア追加-->
      <Setter Property="bh:AutoSelectWhenAnyChildGetsFocus.Enabled" Value="True"></Setter>

      <Style.Triggers>

        <!--この子は廃止-->
        <!--<Trigger Property="IsKeyboardFocusWithin" Value="True">
          <Setter Property="IsSelected" Value="True"/>
        </Trigger>-->

        <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="Background" Value="LightBlue" />
        </Trigger>
        <Trigger Property="IsMouseOver" Value="False">
          <Setter Property="Background" Value="LightGray" />
        </Trigger>
        <Trigger Property="IsSelected" Value="True">
          <Setter Property="Background" Value="Plum" />
        </Trigger>

      </Style.Triggers>

    </Style>

これでビルドして実行します。TextBoxのフォーカスと連動してListBoxItemが選択されます。
Trigger追加

関羽を選択してみましょう。
選択成功

今度は大丈夫そうです。

まとめ

TextBoxを含んだListBoxItemでTextBoxのフォーカスに連動してListBoxItemも選択させたい。だけどSelectedItem等を使って色々と処理もしたい。そういう場合はIsKeyboardFocusWithinと連動させてIsSelectedを操作するのでは無くBehaviorやコードビハインドを使う。

18
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
13