web-dev-qa-db-ja.com

MVVMを使用して選択したTreeViewItemを取得する

そこで誰かがWPFTreeViewの使用を提案し、私は「そうだ、それは正しいアプローチのようだ」と思った。何時間も経った今、私はこのコントロールを使用することがどれほど困難であったかを信じられません。たくさんの調査を通じて、TreeView`コントロールを機能させることができましたが、選択したアイテムをビューモデルに取り込むための「適切な」方法を見つけることができません。コードから選択した項目を設定する必要はありません。ユーザーがどのアイテムを選択したかを知るためにビューモデルが必要です。

これまでのところ、私はこのXAMLを持っていますが、それ自体はあまり直感的ではありません。これはすべてUserControl.Resourcesタグ内にあります。

<CollectionViewSource x:Key="cvs" Source="{Binding ApplicationServers}">
    <CollectionViewSource.GroupDescriptions>
        <PropertyGroupDescription PropertyName="DeploymentEnvironment"/>
    </CollectionViewSource.GroupDescriptions>
</CollectionViewSource>

<!-- Our leaf nodes (server names) -->
<DataTemplate x:Key="serverTemplate">
    <TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>

<!-- Note: The Items path refers to the items in the CollectionViewSource group (our servers).
           The Name path refers to the group name. -->
<HierarchicalDataTemplate x:Key="categoryTemplate"
                          ItemsSource="{Binding Path=Items}"
                          ItemTemplate="{StaticResource serverTemplate}">
    <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
</HierarchicalDataTemplate>

そして、これがツリービューです。

<TreeView DockPanel.Dock="Bottom" ItemsSource="{Binding Source={StaticResource cvs}, Path=Groups}"
              ItemTemplate="{StaticResource categoryTemplate}">
            <Style TargetType="TreeViewItem">
                <Setter Property="IsSelected" Value="{Binding Path=IsSelected}"/>
            </Style>
        </TreeView>

これにより、サーバーが環境(dev、QA、prod)ごとに正しく表示されます。ただし、SOで選択したアイテムを取得する方法はさまざまで、多くは複雑で難しいものです。選択したアイテムを取得する方法は簡単ですか?私のビューモデル?

注:TreeView`にはSelectedItemプロパティがありますが、これは読み取り専用です。私にとってイライラするのは、読み取り専用で問題がないことです。コードで変更したくありません。しかし、コンパイラが読み取り専用であると文句を言うので、私はそれを使用できません。

このようなことをするための一見エレガントな提案もありました:

<ContentPresenter Content="{Binding ElementName=treeView1, Path=SelectedItem}" />

そして、私はこの質問をしました:「ビューモデルはどのようにしてこの情報を取得できますか?ContentPresenterが選択されたアイテムを保持していることがわかりますが、それをビューモデルにどのように取得しますか?」しかし、まだ答えはありません。

したがって、私の全体的な質問は、「選択したアイテムをビューモデルに取得するための単純方法はありますか?」です。

18
Bob Horn

やりたいことをするために、ItemContainerStyleTreeViewを変更できます。

<TreeView>
  <TreeView.ItemContainerStyle>
    <Style TargetType="TreeViewItem">
      <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
    </Style>
  </TreeView.ItemContainerStyle>
</TreeView>

次に、ビューモデル(ツリー内の各アイテムのビューモデル)は、ブール値のIsSelectedプロパティを公開する必要があります。

特定のTreeViewItemが展開されるかどうかを制御できるようにしたい場合は、そのプロパティにもセッターを使用できます。

<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>

次に、ビューモデルはブール値のIsExpandedプロパティを公開する必要があります。

これらのプロパティは双方向で機能するため、ユーザーがツリー内のノードを選択すると、ビューモデルのIsSelectedプロパティがtrueに設定されることに注意してください。一方、ビューモデルでIsSelectedをtrueに設定すると、そのビューモデルのツリー内のノードが選択されます。そして同様に拡張されました。

ツリー内の各アイテムのビューモデルがない場合は、それを取得する必要があります。ビューモデルがないということは、モデルオブジェクトをビューモデルとして使用していることを意味しますが、これを機能させるには、これらのオブジェクトにIsSelectedプロパティが必要です。

親ビューモデル(SelectedItemにバインドし、子ビューモデルのコレクションを持つプロパティ)でTreeViewプロパティを公開するには、次のように実装できます。

public ChildViewModel SelectedItem {
  get { return Items.FirstOrDefault(i => i.IsSelected); }
}

ツリー上の個々のアイテムの選択を追跡したくない場合でも、SelectedItemTreeViewプロパティを使用できます。ただし、「MVVMスタイル」で実行できるようにするには、ブレンド動作を使用する必要があります(さまざまなNuGetパッケージとして利用可能-「ブレンドインタラクティブ機能」を検索してください)。

ここに、選択したアイテムがツリー内で変更されるたびにコマンドを呼び出すEventTriggerを追加しました。

<TreeView x:Name="treeView">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="SelectedItemChanged">
      <i:InvokeCommandAction
        Command="{Binding SetSelectedItemCommand}"
        CommandParameter="{Binding SelectedItem, ElementName=treeView}"/>
    </i:EventTrigger>
  </i:Interaction.Triggers>
</TreeView>

SetSelectedItemCommandDataContextにプロパティTreeViewを追加して、ICommandを返す必要があります。ツリービューの選択されたアイテムが変更されると、コマンドのExecuteメソッドが、選択されたアイテムをパラメーターとして呼び出されます。コマンドを作成する最も簡単な方法は、おそらくDelegateCommandを使用することです(WPFの一部ではないため、Googleで実装を取得します)。

不格好なコマンドなしで双方向バインディングを可能にするおそらくより良い代替手段は、ここStackOverflowでSteveGreatrexによって提供された BindableSelectedItemBehavior を使用することです。

29

私はおそらく SelectedItemChanged イベントを使用して、VMにそれぞれのプロパティを設定します。

5
H.B.

マーティンの答えに基づいて、提案されたソリューションを適用する方法を示す簡単なアプリケーションを作成しました。

サンプルコードはCinchV2フレームワークを使用してMVVMをサポートしていますが、好みのフレームワークを使用するように簡単に変更できます。

興味のある方は、GitHubで コードはこちら

それが役に立てば幸い。

1
daspn

パーティーに少し遅れましたが、今これに出くわしている人にとって、私の解決策は次のとおりでした。

  1. 'System.Windows.Interactivity'への参照を追加します
  2. 次のコードをtreeview要素に追加します。 <i:Interaction.Triggers> <i:EventTrigger EventName="SelectedItemChanged"> <i:InvokeCommandAction Command="{Binding SomeICommand}" CommandParameter="{Binding ElementName=treeviewName, Path=SelectedItem}" /> </i:EventTrigger> </i:Interaction.Triggers>

これにより、標準のMVVM ICommandバインディングを使用して、コードビハインドや長い回避策を使用せずにSelectedItemにアクセスできます。

0
melodiouscode

また、パーティーに遅れますが、MVVMLightユーザーの代替手段として:

  1. TreeViewItemをViewModelにバインドして、IsSelectedプロパティの変更を取得します。
  2. SelectedItem ViewModelまたはModelアイテムを送信するMVVMLightメッセージ(PropertyChangeMes​​sageなど)を作成します
  3. このメッセージをリッスンするには、TreeViewホスト(または必要に応じて他のViemModel)を登録します。

全体の実装は非常に高速で、正常に機能します。

ここで、IsSelectedプロパティ(SourceItemは選択されたViewModelアイテムのModel部分です):

       Public Property IsSelected As Boolean
        Get
            Return _isSelected
        End Get
        Set(value As Boolean)
            If Me.HasImages Then
                _isSelected = value
                OnPropertyChanged("IsSelected")
                Messenger.Default.Send(Of SelectedImageFolderChangedMessage)(New SelectedImageFolderChangedMessage(Me, SourceItem, "SelectedImageFolder"))
            Else
                Me.IsExpanded = Not Me.IsExpanded
            End If
        End Set
    End Property

そしてここでVMホストコード:

    Messenger.Default.Register(Of SelectedImageFolderChangedMessage)(Me, AddressOf NewSelectedImageFolder)

    Private Sub NewSelectedImageFolder(msg As SelectedImageFolderChangedMessage)
        If msg.PropertyName = "SelectedImageFolder" Then
            Me.SelectedFolderItem = msg.NewValue
        End If
    End Sub
0
Mr.CQ