SelectedPath
WPF と同期するプロパティを作成しようとしています (たとえば、ビューモデルで) TreeView
。理論は次のとおりです。
- ツリー ビューで選択した項目が変更されるたびに (
SelectedItem
プロパティ/SelectedItemChanged
イベント)、SelectedPath
プロパティを更新して、選択したツリー ノードへのパス全体を表す文字列を格納します。 - プロパティが変更されるたびに
SelectedPath
、パス文字列で示されるツリー ノードを見つけ、そのツリー ノードへのパス全体を展開し、以前に選択したノードの選択を解除した後、それを選択します。
DataNode
このすべてを再現可能にするために、すべてのツリー ノードがタイプ(以下を参照) であり、すべてのツリー ノードがその親ノードの子の中で一意の名前を持ち、パス セパレータが単一であると仮定します。スラッシュ/
.
SelectedPath
イベントでプロパティを更新するSelectedItemChange
ことは問題ではありません。次のイベント ハンドラは問題なく動作します。
void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
DataNode selNode = e.NewValue as DataNode;
if (selNode == null) {
vm.SelectedPath = null;
} else {
vm.SelectedPath = selNode.FullPath;
}
}
しかし、私はその逆を適切に機能させることができません。したがって、以下の一般化および最小化されたコード サンプルに基づく私の質問は、次のとおりです。WPF の TreeView にプログラムによる項目の選択を尊重させるにはどうすればよいですか?
さて、私はどこまで来ましたか?まず、TreeView のSelectedItem
プロパティは読み取り専用なので、直接設定することはできません。これについて詳しく説明している SO に関する多数の質問 ( this、thisまたはthisなど) を見つけて読みました。また、この blogpost、this article、またはthis blogpostなどの他のサイトのリソースも見つけて読みました。
これらのリソースのほとんどすべては、のプロパティをビューモデルの基になるツリー ノード オブジェクトの同等のプロパティにTreeViewItem
バインドするためのスタイルを定義することを示しています。場合によっては (たとえばhereとhere )、バインドは双方向で行われますが、場合によっては (たとえばhereとhere ) 一方向のバインドになります。これを一方向バインディングにする意味がわかりません (ツリー ビュー UI がアイテムの選択を解除した場合、その変更はもちろん、基になるビュー モデルに反映されるはずです)。そのため、双方向バインディングを実装しました。バージョン。(通常、同じことが提案されているため、そのためのプロパティも追加しました。)TreeViewItem
IsSelected
IsExpanded
これはTreeViewItem
私が使用しているスタイルです:
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
このスタイルが実際に適用されていることを確認しました (Background
プロパティをに設定するセッターを追加するとRed
、すべてのツリー ビュー アイテムが赤い背景で表示されます)。
そして、これが単純化され一般化されたDataNode
クラスです。
public class DataNode : INotifyPropertyChanged
{
public DataNode(DataNode parent, string name)
{
this.parent = parent;
this.name = name;
}
private readonly DataNode parent;
private readonly string name;
public string Name {
get {
return name;
}
}
public override string ToString()
{
return name;
}
public string FullPath {
get {
if (parent != null) {
return parent.FullPath + "/" + name;
} else {
return "/" + name;
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private DataNode[] children;
public IEnumerable<DataNode> Children {
get {
if (children == null) {
children = DataSource.GetChildNodes(FullPath).Select(s => new DataNode(this, s)).ToArray();
}
return children;
}
}
private bool isSelected;
public bool IsSelected {
get {
return isSelected;
}
set {
if (isSelected != value) {
isSelected = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsSelected"));
}
}
}
private bool isExpanded;
public bool IsExpanded {
get {
return isExpanded;
}
set {
if (isExpanded != value) {
isExpanded = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
}
}
}
public void ExpandPath()
{
if (parent != null) {
parent.ExpandPath();
}
IsExpanded = true;
}
}
ご覧のとおり、各ノードには名前、親ノード (存在する場合) への参照があり、子ノードを遅延して初期化しますが、1回IsSelected
だけIsExpanded
ですPropertyChanged
。INotifyPropertyChanged
.
したがって、私のビューモデルでは、SelectedPath
プロパティは次のように実装されています。
public string SelectedPath {
get {
return selectedPath;
}
set {
if (selectedPath != value) {
DataNode prevSel = NodeByPath(selectedPath);
if (prevSel != null) {
prevSel.IsSelected = false;
}
selectedPath = value;
DataNode newSel = NodeByPath(selectedPath);
if (newSel != null) {
newSel.ExpandPath();
newSel.IsSelected = true;
}
OnPropertyChanged(new PropertyChangedEventArgs("SelectedPath"));
}
}
}
NodeByPath
メソッドは正しく (これを確認しました)、指定されたパス文字列のインスタンスを取得しますDataNode
。それにもかかわらず、ビューモデルのプロパティにaTextBox
をバインドすると、アプリケーションを実行して次の動作を確認できます。SelectedPath
- タイプ
/0
=> アイテム/0
が選択され、展開されます - type
/0/1/2
=> item/0
は選択されたままですが、item/0/1/2
は展開されます。
同様に、選択したパスを最初に に設定すると/0/1
、そのアイテムは正しく選択されて展開されますが、その後のパス値では、アイテムは展開されるだけで、選択されることはありません。
しばらくデバッグしたところ、行内のセッターの再帰呼び出しが問題かと思いましたが、そのコマンドの実行中にセッターコードの実行を禁止するフラグを追加しても、プログラムの動作は変わらないようでした。まったく。SelectedPath
prevSel.IsSelected = false;
それで、私はここで何が間違っていますか?これらすべてのブログ投稿で提案されていることとは異なることをどこで行っているのかわかりません。TreeViewIsSelected
は、新しく選択された項目の新しい値について何らかの方法で通知する必要がありますか?
便宜上、自己完結型の最小限の例を構成する 5 つのファイルすべての完全なコードを示します (この例では、データ ソースは明らかに偽のデータを返しますが、定数ツリーを返すため、上記のテスト ケースが再現可能になります)。
DataNode.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
namespace TreeViewTest
{
public class DataNode : INotifyPropertyChanged
{
public DataNode(DataNode parent, string name)
{
this.parent = parent;
this.name = name;
}
private readonly DataNode parent;
private readonly string name;
public string Name {
get {
return name;
}
}
public override string ToString()
{
return name;
}
public string FullPath {
get {
if (parent != null) {
return parent.FullPath + "/" + name;
} else {
return "/" + name;
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private DataNode[] children;
public IEnumerable<DataNode> Children {
get {
if (children == null) {
children = DataSource.GetChildNodes(FullPath).Select(s => new DataNode(this, s)).ToArray();
}
return children;
}
}
private bool isSelected;
public bool IsSelected {
get {
return isSelected;
}
set {
if (isSelected != value) {
isSelected = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsSelected"));
}
}
}
private bool isExpanded;
public bool IsExpanded {
get {
return isExpanded;
}
set {
if (isExpanded != value) {
isExpanded = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
}
}
}
public void ExpandPath()
{
if (parent != null) {
parent.ExpandPath();
}
IsExpanded = true;
}
}
}
DataSource.cs
using System;
using System.Collections.Generic;
namespace TreeViewTest
{
public static class DataSource
{
public static IEnumerable<string> GetChildNodes(string path)
{
if (path.Length < 40) {
for (int i = 0; i < path.Length + 2; i++) {
yield return (2 * i).ToString();
yield return (2 * i + 1).ToString();
}
}
}
}
}
ViewModel.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
namespace TreeViewTest
{
public class ViewModel : INotifyPropertyChanged
{
private readonly DataNode[] rootNodes = DataSource.GetChildNodes("").Select(s => new DataNode(null, s)).ToArray();
public IEnumerable<DataNode> RootNodes {
get {
return rootNodes;
}
}
private DataNode NodeByPath(string path)
{
if (path == null) {
return null;
} else {
string[] levels = selectedPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
IEnumerable<DataNode> currentAvailable = rootNodes;
for (int i = 0; i < levels.Length; i++) {
string node = levels[i];
foreach (DataNode next in currentAvailable) {
if (next.Name == node) {
if (i == levels.Length - 1) {
return next;
} else {
currentAvailable = next.Children;
}
break;
}
}
}
return null;
}
}
private string selectedPath;
public string SelectedPath {
get {
return selectedPath;
}
set {
if (selectedPath != value) {
DataNode prevSel = NodeByPath(selectedPath);
if (prevSel != null) {
prevSel.IsSelected = false;
}
selectedPath = value;
DataNode newSel = NodeByPath(selectedPath);
if (newSel != null) {
newSel.ExpandPath();
newSel.IsSelected = true;
}
OnPropertyChanged(new PropertyChangedEventArgs("SelectedPath"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
}
}
Window1.xaml
<Window x:Class="TreeViewTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TreeViewTest" Height="450" Width="600"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TreeView ItemsSource="{Binding RootNodes}" SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding .}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<TextBox Grid.Row="1" Text="{Binding SelectedPath, Mode=TwoWay}"/>
</Grid>
</Window>
Window1.xaml.cs
using System;
using System.Windows;
namespace TreeViewTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = vm;
}
void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
DataNode selNode = e.NewValue as DataNode;
if (selNode == null) {
vm.SelectedPath = null;
} else {
vm.SelectedPath = selNode.FullPath;
}
}
private readonly ViewModel vm = new ViewModel();
}
}