我有一个表示某些项目的树视图。这棵树总是两层深。子项的右键单击菜单具有“向上移动”命令。用户界面允许您向上移动子项,即使它是其父项的第一项,只要在父级别上有另一项,在所选项的父项之上。
显而易见的方法是获取所选项目的父项,并查看其上方是否有项目。但是,在WPF中获取所选项目的父项是微不足道的。同样,显而易见的(对于WPF初学者,无论如何)方法是获取所选项目的TreeViewItem
,其具有Parent
属性。不幸的是,这也很难做到。
从那些说it’s hard because I’m doing it wrong的人那里得到提示,我决定问那些对WPF更有经验的人:这是做正确的,非硬的方法吗?逻辑上它很简单,但我无法弄清楚处理WPF API的正确方法。
答案 0 :(得分:5)
使用Wpf TreeView做这种事情是非常正确的。其中一个关键部分是Wpf为您提供的灵活性 - 您可以在自定义TreeView中覆盖ItemContainerGenerator,例如,您的树视图可能实际上不包含TreeViewItem对象。即,在类似的Winforms控件中找不到相同的固定层次结构。
起初看起来真的很反直,MS真的很遗憾MS没有花更多的时间来解释如何让这种事情以不会导致挫折的方式发挥作用。
自从拥抱MVVM以来,我们已经取得了巨大的成功 - 我们总是为绑定到UI的类创建一个ViewModel,毫无例外 - 它更容易接入新的以后的功能。
如果您的树视图绑定了底层视图模型(如果必须,甚至是模型项),并将树视图视为观察者,那么使用Wpf TreeView和其他Wpf控件可以更好地相处太。在树形层次结构的实际术语中,您将拥有TreeView可视化的viewmodel对象层次结构 - 其中每个子项都有一个句柄返回其父级,并且每个父级都有一个子视图模型的集合。然后,您将为每个项目提供分层数据模板,其中ItemsSource是ChildCollection。然后,您针对ViewModel启动“MoveUp”命令,并负责进行更改 - 如果您使用基于ObservableCollection的集合(或实现INotifyCollectionChanged),则TreeView会自动更新以反映新的层次结构。
从ViewModel驱动功能,并将UI视为反映ViewModel层次结构和属性的薄层,使代码可以进行高度单元测试 - 代码隐藏中没有代码,您可以经常使用在没有任何UI的情况下测试您的ViewModel功能,从长远来看,它可以提供质量更好的代码。
当我们开始使用Wpf时,对我们的自然反应是ViewModels过度杀伤,但我们的经验(在许多地方没有它们开始)是他们开始在Wpf中快速获得回报,毫无疑问值得付出额外的努力让你的头脑。
你可能还没有发现的一件事,我们发现它真的很痛苦,就是在树视图上设置所选项目 - 现在这不适合胆小的人:)
答案 1 :(得分:1)
“正确”的方式是忘记问题的UI表现形式,而是考虑模型应该如何表示它。你的UI后面有一个模型,对吗?
然后,您的UI将绑定到模型上的相应属性。
答案 2 :(得分:1)
我可能在这里遗漏了一些东西,但我要做的是将SelectedIndex作为命令参数传递给命令的CanExecute方法的绑定。然后使用它来决定命令是否启用。
问题可能是上传菜单的datacontext在加载后没有改变,因为上下文菜单不在可视树中。我通常使用this method通过静态资源将datacontext公开给不在可视树中的项目。我实际上写了answer to a question about this earlier today。
我真的觉得我错过了什么。你能解释一下为什么这不起作用吗?
好的,我读过关于TreeViews的abit,但仍然没有真正理解这个问题是什么。所以我继续做了一个例子并设法让它发挥作用。
我的第一步是阅读This article by Josh Smith about treeviews。它讨论了为每个项类型创建视图模型并公开IsSelected和IsExpanded等属性,然后在xaml中绑定它们。这允许您在视图模型中访问treeviewitem的属性。
读完之后我开始工作:
#region Models
public class Person
{
public string FirstName { get; set; }
public string SurName { get; set; }
public int Age { get; set; }
}
public class Actor:Person
{
public decimal Salary { get; set; }
}
public class ActingRole :Person
{
public Actor Actor { get; set; }
}
public class Movie
{
public string Name { get; set; }
public List<ActingRole> Characters { get; set; }
public string PlotSummary { get; set; }
public Movie()
{
Characters = new List<ActingRole>();
}
}
#endregion
需要注意的重要一点是,他们都有父和孩子。
这就是我们如何跟踪父母集合中的第一个或最后一个项目。
interface ITreeViewItemViewModel
{
ObservableCollection<TreeViewItemViewModel> Children { get; }
bool IsExpanded { get; set; }
bool IsSelected { get; set; }
TreeViewItemViewModel Parent { get; }
}
public class TreeViewItemViewModel : ITreeViewItemViewModel, INotifyPropertyChanged
{
private ObservableCollection<TreeViewItemViewModel> _children;
private TreeViewItemViewModel _parent;
private bool _isSelected;
private bool _isExpanded;
public TreeViewItemViewModel Parent
{
get
{
return _parent;
}
}
public TreeViewItemViewModel(TreeViewItemViewModel parent = null,ObservableCollection<TreeViewItemViewModel> children = null)
{
_parent = parent;
if (children != null)
_children = children;
else
_children = new ObservableCollection<TreeViewItemViewModel>();
}
public ObservableCollection<TreeViewItemViewModel> Children
{
get
{
return _children;
}
}
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is selected.
/// </summary>
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
}
}
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is expanded.
/// </summary>
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != _isExpanded)
{
_isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
}
}
#region INotifyPropertyChanged Members
/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
#endregion // INotifyPropertyChanged Members
#region Debugging Aides
/// <summary>
/// Warns the developer if this object does not have
/// a public property with the specified name. This
/// method does not exist in a Release build.
/// </summary>
[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
// Verify that the property name matches a real,
// public, instance property on this object.
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
string msg = "Invalid property name: " + propertyName;
if (this.ThrowOnInvalidPropertyName)
throw new Exception(msg);
else
Debug.Fail(msg);
}
}
/// <summary>
/// Returns whether an exception is thrown, or if a Debug.Fail() is used
/// when an invalid property name is passed to the VerifyPropertyName method.
/// The default value is false, but subclasses used by unit tests might
/// override this property's getter to return true.
/// </summary>
protected virtual bool ThrowOnInvalidPropertyName { get; private set; }
#endregion // Debugging Aides
}
public class MovieViewModel : TreeViewItemViewModel
{
private Movie _movie;
public MovieViewModel(Movie movie)
{
_movie = movie;
foreach(ActingRole a in _movie.Characters)
Children.Add(new ActingRoleViewModel(a,this));
}
public string Name
{
get
{
return _movie.Name;
}
set
{
_movie.Name = value;
OnPropertyChanged("Name");
}
}
public List<ActingRole> Characters
{
get
{
return _movie.Characters;
}
set
{
_movie.Characters = value;
OnPropertyChanged("Characters");
}
}
public string PlotSummary
{
get
{
return _movie.PlotSummary;
}
set
{
_movie.PlotSummary = value;
OnPropertyChanged("PlotSummary");
}
}
}
public class ActingRoleViewModel : TreeViewItemViewModel
{
private ActingRole _role;
public ActingRoleViewModel(ActingRole role, MovieViewModel parent):base (parent)
{
_role = role;
Children.Add(new ActorViewModel(_role.Actor, this));
}
public string FirstName
{
get
{
return _role.FirstName;
}
set
{
_role.FirstName = value;
OnPropertyChanged("FirstName");
}
}
public string SurName
{
get
{
return _role.SurName;
}
set
{
_role.SurName = value;
OnPropertyChanged("Surname");
}
}
public int Age
{
get
{
return _role.Age;
}
set
{
_role.Age = value;
OnPropertyChanged("Age");
}
}
public Actor Actor
{
get
{
return _role.Actor;
}
set
{
_role.Actor = value;
OnPropertyChanged("Actor");
}
}
}
public class ActorViewModel:TreeViewItemViewModel
{
private Actor _actor;
private ActingRoleViewModel _parent;
public ActorViewModel(Actor actor, ActingRoleViewModel parent):base (parent)
{
_actor = actor;
}
public string FirstName
{
get
{
return _actor.FirstName;
}
set
{
_actor.FirstName = value;
OnPropertyChanged("FirstName");
}
}
public string SurName
{
get
{
return _actor.SurName;
}
set
{
_actor.SurName = value;
OnPropertyChanged("Surname");
}
}
public int Age
{
get
{
return _actor.Age;
}
set
{
_actor.Age = value;
OnPropertyChanged("Age");
}
}
public decimal Salary
{
get
{
return _actor.Salary;
}
set
{
_actor.Salary = value;
OnPropertyChanged("Salary");
}
}
}
这里需要注意的是我有一个SelectedItem属性。我通过订阅所有 viewmodel的属性更改事件然后获取所选的那个来获得此项目。我使用此项来检查它是否是其父项子集合中最后一项的第一项。
在命令启用方法中还要注意我如何判断项目是否在根目录中。这很重要,因为我的mainwindowviewmodel不是TreeViewItemViewModel,并且没有实现Children属性。显然,对于您的程序,您将需要另一种方法来整理根。您可能希望在名为root的TreeViewItemViewModel中放置一个布尔变量,如果该项没有父项,则可以将其设置为true。
public class MainWindowViewModel : INotifyPropertyChanged
{
private ObservableCollection<MovieViewModel> _movieViewModels;
public ObservableCollection<MovieViewModel> MovieViewModels
{
get
{
return _movieViewModels;
}
set
{
_movieViewModels = value;
OnPropertyChanged("MovieViewModels");
}
}
private TreeViewItemViewModel SelectedItem { get; set; }
public MainWindowViewModel()
{
InitializeMovies();
InitializeCommands();
InitializePropertyChangedHandler((from f in MovieViewModels select f as TreeViewItemViewModel).ToList());
}
public ICommand MoveItemUpCmd { get; protected set; }
public ICommand MoveItemDownCmd { get; protected set; }
private void InitializeCommands()
{
//Initializes the command
this.MoveItemUpCmd = new RelayCommand(
(param) =>
{
this.MoveItemUp();
},
(param) => { return this.CanMoveItemUp; }
);
this.MoveItemDownCmd = new RelayCommand(
(param) =>
{
this.MoveItemDown();
},
(param) => { return this.CanMoveItemDown; }
);
}
public void MoveItemUp()
{
}
private bool CanMoveItemUp
{
get
{
if (SelectedItem != null)
if (typeof(MovieViewModel) == SelectedItem.GetType())
{
return MovieViewModels.IndexOf((MovieViewModel)SelectedItem) > 0;
}
else
{
return SelectedItem.Parent.Children.IndexOf(SelectedItem) > 0;
}
else
return false;
}
}
public void MoveItemDown()
{
}
private bool CanMoveItemDown
{
get
{
if (SelectedItem != null)
if (typeof(MovieViewModel) == SelectedItem.GetType())
{
return MovieViewModels.IndexOf((MovieViewModel)SelectedItem) < (MovieViewModels.Count - 1);
}
else
{
var test = SelectedItem.Parent.Children.IndexOf(SelectedItem);
return SelectedItem.Parent.Children.IndexOf(SelectedItem) < (SelectedItem.Parent.Children.Count - 1);
}
else
return false;
}
}
private void InitializeMovies()
{
MovieViewModels = new ObservableCollection<MovieViewModel>();
//Please note all this data is pure speculation. Prolly have spelling mistakes aswell
var TheMatrix = new Movie();
TheMatrix.Name = "The Matrix";
TheMatrix.Characters.Add(new ActingRole(){FirstName = "Neo", SurName="", Age=28, Actor=new Actor(){FirstName="Keeanu", SurName="Reeves", Age=28, Salary=2000000}});
TheMatrix.Characters.Add(new ActingRole() { FirstName = "Morpheus", SurName = "", Age = 34, Actor = new Actor() { FirstName = "Lorance", SurName = "Fishburn", Age = 34, Salary = 800000 } });
TheMatrix.PlotSummary = "A programmer by day, and hacker by night searches for the answer to a question that has been haunting him: What is the matrix? The answer soon finds him and his world is turned around";
var FightClub = new Movie();
FightClub.Name = "Fight Club";
FightClub.Characters.Add(new ActingRole() { FirstName = "", SurName = "", Age = 28, Actor = new Actor() { FirstName = "Edward", SurName = "Norton", Age = 28, Salary = 1300000 } });
FightClub.Characters.Add(new ActingRole() { FirstName = "Tylar", SurName = "Durden", Age = 27, Actor = new Actor() { FirstName = "Brad", SurName = "Pit", Age = 27, Salary = 3500000 } });
FightClub.PlotSummary = "A man suffers from insomnia, and struggles to find a cure. In desperation he starts going to testicular cancer surviver meetings, and after some weeping finds he sleeps better. Meanwhile a new aquantance, named Tylar Durden is about so show him a much better way to deal with his problems.";
MovieViewModels.Add(new MovieViewModel(TheMatrix));
MovieViewModels.Add(new MovieViewModel(FightClub));
}
private void InitializePropertyChangedHandler(IList<TreeViewItemViewModel> treeViewItems)
{
foreach (TreeViewItemViewModel t in treeViewItems)
{
t.PropertyChanged += TreeViewItemviewModel_PropertyChanged;
InitializePropertyChangedHandler(t.Children);
}
}
private void TreeViewItemviewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsSelected" && ((TreeViewItemViewModel)sender).IsSelected)
{
SelectedItem = ((TreeViewItemViewModel)sender);
}
}
#region INotifyPropertyChanged Members
/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
#endregion // INotifyPropertyChanged Members
#region Debugging Aides
/// <summary>
/// Warns the developer if this object does not have
/// a public property with the specified name. This
/// method does not exist in a Release build.
/// </summary>
[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
// Verify that the property name matches a real,
// public, instance property on this object.
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
string msg = "Invalid property name: " + propertyName;
if (this.ThrowOnInvalidPropertyName)
throw new Exception(msg);
else
Debug.Fail(msg);
}
}
/// <summary>
/// Returns whether an exception is thrown, or if a Debug.Fail() is used
/// when an invalid property name is passed to the VerifyPropertyName method.
/// The default value is false, but subclasses used by unit tests might
/// override this property's getter to return true.
/// </summary>
protected virtual bool ThrowOnInvalidPropertyName { get; private set; }
#endregion // Debugging Aides
}
注意treeviewitem树视图中的样式。这是我们将所有TreeViewItem属性绑定到TreeviewItemViewModel中创建的属性的地方。
contextmenu的MenuItems的命令属性通过DataContextBridge(类似于ElementSpy,两个Josh Smith创建)绑定到命令。这是因为contextmenu不在可视树中,因此无法绑定到viewmodel。
另请注意,我为每个创建的viewmodel类型都有不同的HierarchicalDataTemplate。这允许我绑定到将在树视图中显示的不同类型的不同属性。
<TreeView Margin="5,5,5,5" HorizontalAlignment="Stretch" ItemsSource="{Binding Path=MovieViewModels,UpdateSourceTrigger=PropertyChanged}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu DataContext="{StaticResource DataContextBridge}">
<MenuItem Header="Move _Up"
Command="{Binding DataContext.MoveItemUpCmd}" />
<MenuItem Header="Move _Down"
Command="{Binding DataContext.MoveItemDownCmd}" />
</ContextMenu>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type classes:MovieViewModel}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type classes:ActingRoleViewModel}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="5,0,0,0" Text="{Binding FirstName}"/>
<TextBlock Margin="5,0,5,0" Text="{Binding SurName}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type classes:ActorViewModel}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="5,0,0,0" Text="{Binding FirstName}"/>
<TextBlock Margin="5,0,5,0" Text="{Binding SurName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>