我再次需要你的帮助。
我正在构建一个 WPF 应用程序,其中我有一个页面的 MainViewModel,其中包含树视图。此树视图绑定到由 MainViewModel 创建的 ProjectTreeViewModel。
现在,我的ProjectTreeViewModel捕获了一个点击事件(使用中继命令(,告诉它点击了哪个节点。
我需要在我的主视图模型中提供此信息。我如何将其转移到那里?
编辑...一个可运行的示例
树中显示的一些数据:
using WpfApp1.Models;
namespace WpfApp1.Dataprovider
{
class PlcAddressData
{
public static PlcAddress GetPlcRootItems(string projectName)
{
if (string.IsNullOrWhiteSpace(projectName))
projectName = "Projekt-Datenpunkte";
return new PlcAddress
{
Name = projectName,
NodeId = 0,
Children =
{
new PlcAddress
{
Name = "Allgemein",
Comment = "allgemeine Datenpunkte",
NodeId = 1,
ParentNodeId = 0
},
new PlcAddress
{
Name = "Infrastruktur",
Comment = "interne Datenpunkte der Infrastruktur",
ParentNodeId = 0,
NodeId = 2
},
new PlcAddress
{
Name = "lokale IOs",
Comment = "Datenpunkte der SPS-Baugruppe",
ParentNodeId = 0,
NodeId = 3,
Children =
{
new PlcAddress
{
Name = "IO 0",
Comment = "first Channel of Plc-IO-Card",
NodeId = 4,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 1",
Comment = "second Channel of Plc-IO-Card",
NodeId = 5,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 2",
Comment = "third Channel of Plc-IO-Card",
NodeId = 6,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 3",
Comment = "forth Channel of Plc-IO-Card",
NodeId = 7,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 4",
Comment = "fifth Channel of Plc-IO-Card",
NodeId = 8,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 5",
Comment = "sixth Channel of Plc-IO-Card",
NodeId = 9,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 6",
Comment = "seventh Channel of Plc-IO-Card",
NodeId = 10,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 7",
Comment = "eighth Channel of Plc-IO-Card",
NodeId = 11,
ParentNodeId = 3
}
}
}
}
};
}
}
}
PlcAddress-model(树视图项的数据(:
using System.Collections.Generic;
namespace WpfApp1.Models
{
public class PlcAddress
{
private List<PlcAddress> _children = new List<PlcAddress>();
public List<PlcAddress> Children
{
get { return _children; }
set { _children = value; }
}
public int NodeId { get; set; }
public int ParentNodeId { get; set; }
public string Name { get; set; }
public string Comment { get; set; }
}
}
中继命令:
using System;
using System.Windows.Input;
namespace WpfApp1.ViewModels.Commands
{
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion
#region Constructors
public RelayCommand(Action<object> execute) : this(execute, null){ }
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
_execute = execute ?? throw new ArgumentNullException("execute");
_canExecute = canExecute;
}
#endregion
#region ICommand Members
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion
}
}
主视图模型:
using WpfApp1.Models;
namespace WpfApp1.ViewModels
{
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
LoadProjectTree();
}
private void LoadProjectTree()
{
PlcAddress RootItem = Dataprovider.PlcAddressData.GetPlcRootItems("Parent Node of Project");
_projectTree = new ProjectTreeviewModel(RootItem);
_projectTree.PropertyChanged += ProjectTreePropertyChanged;
}
private void ProjectTreePropertyChanged(object sender, PropertyChangedEventArgs e)
{
ProjectTreeviewModel selectedNode = (ProjectTreeviewModel)sender;
System.Console.WriteLine("selectedNode changed:" + selectedNode.SelectedNode);
SelectedNode = selectedNode.SelectedNode;
//MessageBox.Show("Some Property changed");
}
#region Properties
private string _selectedNode;
public string SelectedNode {
get { return _selectedNode; }
set
{
_selectedNode = value;
OnPropertyChanged("SelectedNode");
}
}
private ProjectTreeviewModel _projectTree;
public ProjectTreeviewModel ProjectTree
{
get { return _projectTree; }
}
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
要在树中显示为项的PlcAddressViewModel
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using WpfApp1.Models;
namespace WpfApp1.ViewModels
{
public class PlcAddressViewModel : INotifyPropertyChanged
{
#region Data
private Collection<PlcAddressViewModel> _children;
readonly PlcAddressViewModel _parent;
readonly PlcAddress _plcAddress;
bool _isExpanded;
bool _isSelected;
#endregion Data
#region Constructors
public PlcAddressViewModel(PlcAddress plcAddress) : this(plcAddress, null)
{
}
private PlcAddressViewModel(PlcAddress plcAddress, PlcAddressViewModel parent)
{
_parent = parent;
_plcAddress = plcAddress;
_children = new Collection<PlcAddressViewModel>(
(from child in _plcAddress.Children
select new PlcAddressViewModel(child, this))
.ToList<PlcAddressViewModel>());
}
#endregion Constructors
#region AddressProperties
public Collection<PlcAddressViewModel> Children
{
get { return _children; }
set { _children = value; }
}
public string Name
{
get { return _plcAddress.Name; }
}
public string Comment
{
get { return _plcAddress.Comment; }
}
#endregion AddressProperties
#region Presentation Members
#region IsExpanded
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != _isExpanded)
{
_isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root
if (_isExpanded && _parent != null)
_parent.IsExpanded = true;
}
}
#endregion IsExpanded
#region IsSelected
public bool IsSelected
{
get
{
if (_isSelected)
{
//Console.WriteLine("Nodeselected: " + this._plcAddress.Name);
}
return this._isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
}
}
#endregion IsSelected
#region Parent
public PlcAddressViewModel Parent
{
get { return _parent; }
}
#endregion Parent
#endregion Presentation Members
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged Members
}
}
识别所选更改的项目树视图模型
using System;
using System.Collections.ObjectModel;
using WpfApp1.Models;
using WpfApp1.ViewModels.Commands;
namespace WpfApp1.ViewModels
{
public class ProjectTreeviewModel : INotifyPropertyChanged
{
#region Data
public RelayCommand TreeNodeSelected { get; private set; }
readonly ReadOnlyCollection<PlcAddressViewModel> _rootNodes;
readonly PlcAddressViewModel _rootAddress;
#endregion Data
#region Constructor
public ProjectTreeviewModel(PlcAddress rootAddress)
{
_rootAddress = new PlcAddressViewModel(rootAddress);
_rootNodes = new ReadOnlyCollection<PlcAddressViewModel>(
new PlcAddressViewModel[]
{
_rootAddress
});
TreeNodeSelected = new RelayCommand(ExecuteTreeNodeSelected, canExecuteMethod);
}
#endregion Constructor
#region Properties
private string _selectedNode;
public string SelectedNode
{
get { return _selectedNode; }
set
{
_selectedNode = value;
OnPropertyChanged("SelectedNode");
}
}
#endregion
#region RootNode
public ReadOnlyCollection<PlcAddressViewModel> ProjectNode
{
get { return _rootNodes; }
}
#endregion RootNode
#region Commands
private bool canExecuteMethod(object parameter)
{
return true;
}
private void ExecuteTreeNodeSelected(object parameter)
{
PlcAddressViewModel selectedNode = (PlcAddressViewModel)parameter;
Console.WriteLine("Found this node: " + selectedNode.Name);
SelectedNode = selectedNode.Name;
}
#endregion Commands
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
最后但并非最不重要的一点是,MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
xmlns:viewmodels="clr-namespace:WpfApp1.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
>
<DockPanel LastChildFill="True">
<StackPanel Margin="5" Orientation="Horizontal">
<TreeView DataContext="{Binding ProjectTree}" ItemsSource="{Binding ProjectNode}" DockPanel.Dock="Left"
x:Name="ProjectTree" Margin="0 0 2 0" Grid.Column="0">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction Command="{Binding TreeNodeSelected}"
CommandParameter="{Binding ElementName=ProjectTree, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<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"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
<!-- following Texblock is bound to a MainViewModels property -->
<TextBlock Text="{Binding SelectedNode}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DockPanel>
</Window>
。及其代码隐藏:
using System.Windows;
using WpfApp1.ViewModels;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
}
。经过几个小时的实验,我解决了它。解决方案在上面的片段中,我不知道这是否是最好的方法。我以以下方式做到了:
命令方法ExecuteTreeNodeSelected
更改公共属性SelectedNode
。这将触发通知OnPropertyChanged("SelectedNode");
。
在创建 Treeviewmodel 时,在 MainViewModel 中,我向 TreeViewModel_projectTree.PropertyChanged += ProjectTreePropertyChanged;
的 PropertyChanged-event 中添加了一个事件侦听器。此事件更改通知 UI 的主视图模型的SelectedNode
-属性。
感谢您的耐心等待
这是我用于在 TreeView 控件中显示的分层数据的 ViewModel 基类。
/// <summary>
/// A base class for items that can be displayed in a TreeView or other hierarchical display
/// </summary>
public class perTreeViewItemViewModelBase : perViewModelBase
{
// a dummy item used in lazy loading mode, ensuring that each node has at least one child so that the expand button is shown
private static perTreeViewItemViewModelBase LazyLoadingChildIndicator { get; }
= new perTreeViewItemViewModelBase { Caption = "Loading Data ..." };
private bool InLazyLoadingMode { get; set; }
private bool LazyLoadTriggered { get; set; }
private bool LazyLoadCompleted { get; set; }
private bool RequiresLazyLoad => InLazyLoadingMode && !LazyLoadTriggered;
// Has Children been overridden (e.g. to point at some private internal collection)
private bool LazyLoadChildrenOverridden => InLazyLoadingMode && !Equals(LazyLoadChildren, _childrenList);
private readonly perObservableCollection<perTreeViewItemViewModelBase> _childrenList
= new perObservableCollection<perTreeViewItemViewModelBase>();
/// <summary>
/// LazyLoadingChildIndicator ensures a visible expansion toggle button in lazy loading mode
/// </summary>
protected void SetLazyLoadingMode()
{
ClearChildren();
_childrenList.Add(LazyLoadingChildIndicator);
IsExpanded = false;
InLazyLoadingMode = true;
LazyLoadTriggered = false;
LazyLoadCompleted = false;
}
private string _caption;
public string Caption
{
get => _caption;
set => Set(nameof(Caption), ref _caption, value);
}
public void ClearChildren()
{
_childrenList.Clear();
}
/// <summary>
/// Add a new child item to this TreeView item
/// </summary>
/// <param name="child"></param>
public void AddChild(perTreeViewItemViewModelBase child)
{
if (LazyLoadChildrenOverridden)
throw new InvalidOperationException("Don't call AddChild for an item with LazyLoad mode set & LazyLoadChildren has been overridden");
if (_childrenList.Any() && _childrenList.First() == LazyLoadingChildIndicator)
_childrenList.Clear();
_childrenList.Add(child);
SetChildPropertiesFromParent(child);
}
protected void SetChildPropertiesFromParent(perTreeViewItemViewModelBase child)
{
child.Parent = this;
// if this node is checked then all new children added are set checked
if (IsChecked.GetValueOrDefault())
child.SetIsCheckedIncludingChildren(true);
ReCalculateNodeCheckState();
}
protected void ReCalculateNodeCheckState()
{
var item = this;
while (item != null)
{
if (item.Children.Any() && !Equals(item.Children.FirstOrDefault(), LazyLoadingChildIndicator))
{
var hasIndeterminateChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.HasValue);
if (hasIndeterminateChild)
item.SetIsCheckedThisItemOnly(null);
else
{
var hasSelectedChild = item.Children.Any(c => c.IsEnabled && c.IsChecked.GetValueOrDefault());
var hasUnselectedChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.GetValueOrDefault());
if (hasUnselectedChild && hasSelectedChild)
item.SetIsCheckedThisItemOnly(null);
else
item.SetIsCheckedThisItemOnly(hasSelectedChild);
}
}
item = item.Parent;
}
}
private void SetIsCheckedIncludingChildren(bool? value)
{
if (IsEnabled)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
foreach (var child in Children)
if (child.IsEnabled)
child.SetIsCheckedIncludingChildren(value);
}
}
private void SetIsCheckedThisItemOnly(bool? value)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
}
/// <summary>
/// Add multiple children to this TreeView item
/// </summary>
/// <param name="children"></param>
public void AddChildren(IEnumerable<perTreeViewItemViewModelBase> children)
{
foreach (var child in children)
AddChild(child);
}
/// <summary>
/// Remove a child item from this TreeView item
/// </summary>
public void RemoveChild(perTreeViewItemViewModelBase child)
{
_childrenList.Remove(child);
child.Parent = null;
ReCalculateNodeCheckState();
}
public perTreeViewItemViewModelBase Parent { get; private set; }
private bool? _isChecked = false;
public bool? IsChecked
{
get => _isChecked;
set
{
if (Set(nameof(IsChecked), ref _isChecked, value))
{
foreach (var child in Children)
if (child.IsEnabled)
child.SetIsCheckedIncludingChildren(value);
Parent?.ReCalculateNodeCheckState();
}
}
}
private bool _isExpanded;
public bool IsExpanded
{
get => _isExpanded;
set
{
if (Set(nameof(IsExpanded), ref _isExpanded, value) && value && RequiresLazyLoad)
TriggerLazyLoading();
}
}
private bool _isEnabled = true;
public bool IsEnabled
{
get => _isEnabled;
set => Set(nameof(IsEnabled), ref _isEnabled, value);
}
public void TriggerLazyLoading()
{
var unused = DoLazyLoadAsync();
}
private async Task DoLazyLoadAsync()
{
if (LazyLoadTriggered)
return;
LazyLoadTriggered = true;
var lazyChildrenResult = await LazyLoadFetchChildren()
.EvaluateFunctionAsync()
.ConfigureAwait(false);
LazyLoadCompleted = true;
if (lazyChildrenResult.IsCompletedOk)
{
var lazyChildren = lazyChildrenResult.Data;
foreach (var child in lazyChildren)
SetChildPropertiesFromParent(child);
// If LazyLoadChildren has been overridden then just refresh the check state (using the new children)
// and update the check state (in case any of the new children is already set as checked)
if (LazyLoadChildrenOverridden)
ReCalculateNodeCheckState();
else
AddChildren(lazyChildren); // otherwise add the new children to the base collection.
}
RefreshChildren();
}
/// <summary>
/// Get the children for this node, in Lazy-Loading Mode
/// </summary>
/// <returns></returns>
protected virtual Task<perTreeViewItemViewModelBase[]> LazyLoadFetchChildren()
{
return Task.FromResult(new perTreeViewItemViewModelBase[0]);
}
/// <summary>
/// Update the Children property
/// </summary>
public void RefreshChildren()
{
RaisePropertyChanged(nameof(Children));
}
/// <summary>
/// In LazyLoading Mode, the Children property can be set to something other than
/// the base _childrenList collection - e.g as the union ot two internal collections
/// </summary>
public IEnumerable<perTreeViewItemViewModelBase> Children => LazyLoadCompleted
? LazyLoadChildren
: _childrenList;
/// <summary>
/// How are the children held when in lazy loading mode.
/// </summary>
/// <remarks>
/// Override this as required in descendent classes - e.g. if Children is formed from a union
/// of multiple internal child item collections (of different types) which are populated in LazyLoadFetchChildren()
/// </remarks>
protected virtual IEnumerable<perTreeViewItemViewModelBase> LazyLoadChildren => _childrenList;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
// if unselecting we don't care about anything else other than simply updating the property
if (!value)
{
Set(nameof(IsSelected), ref _isSelected, false);
return;
}
// Build a priority queue of operations
//
// All operations relating to tree item expansion are added with priority = DispatcherPriority.ContextIdle, so that they are
// sorted before any operations relating to selection (which have priority = DispatcherPriority.ApplicationIdle).
// This ensures that the visual container for all items are created before any selection operation is carried out.
//
// First expand all ancestors of the selected item - those closest to the root first
//
// Expanding a node will scroll as many of its children as possible into view - see perTreeViewItemHelper, but these scrolling
// operations will be added to the queue after all of the parent expansions.
var ancestorsToExpand = new Stack<perTreeViewItemViewModelBase>();
var parent = Parent;
while (parent != null)
{
if (!parent.IsExpanded)
ancestorsToExpand.Push(parent);
parent = parent.Parent;
}
while (ancestorsToExpand.Any())
{
var parentToExpand = ancestorsToExpand.Pop();
perDispatcherHelper.AddToQueue(() => parentToExpand.IsExpanded = true, DispatcherPriority.ContextIdle);
}
// Set the item's selected state - use DispatcherPriority.ApplicationIdle so this operation is executed after all
// expansion operations, no matter when they were added to the queue.
//
// Selecting a node will also scroll it into view - see perTreeViewItemHelper
perDispatcherHelper.AddToQueue(() => Set(nameof(IsSelected), ref _isSelected, true), DispatcherPriority.ApplicationIdle);
// note that by rule, a TreeView can only have one selected item, but this is handled automatically by
// the control - we aren't required to manually unselect the previously selected item.
// execute all of the queued operations in descending DispatcherPriority order (expansion before selection)
var unused = perDispatcherHelper.ProcessQueueAsync();
}
}
public override string ToString()
{
return Caption;
}
/// <summary>
/// What's the total number of child nodes beneath this one
/// </summary>
public int ChildCount => Children.Count() + Children.Sum(c => c.ChildCount);
}
除其他功能外,它还包含一个 Parent 属性,因此数据结构中的每个项都知道其直接祖先。
我没有使用事件来处理被单击的项,而是创建了一个行为来将可绑定的选定项属性添加到 TreeView。这允许检测直接鼠标单击以外的方式的选择更改,例如用户按下箭头键。
public class perTreeViewHelper : Behavior<TreeView>
{
public object BoundSelectedItem
{
get => GetValue(BoundSelectedItemProperty);
set => SetValue(BoundSelectedItemProperty, value);
}
public static readonly DependencyProperty BoundSelectedItemProperty =
DependencyProperty.Register("BoundSelectedItem",
typeof(object),
typeof(perTreeViewHelper),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnBoundSelectedItemChanged));
private static void OnBoundSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue is perTreeViewItemViewModelBase item)
item.IsSelected = true;
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
base.OnDetaching();
}
private void OnTreeViewSelectedItemChanged(object obj, RoutedPropertyChangedEventArgs<object> args)
{
BoundSelectedItem = args.NewValue;
}
}
有关我的博客文章的更多详细信息