Как изменить свойство parent-viewmodel - PullRequest
0 голосов
/ 29 мая 2020

снова Мне нужна ваша помощь.

Я создаю приложение WPF, где у меня есть MainViewModel для страницы, которая содержит древовидное представление. Это древовидное представление привязано к ProjectTreeViewModel, который создается MainViewModel.

Теперь моя ProjectTreeViewModel улавливает событие щелчка (с помощью команды реле), которое сообщает ему, какой узел был нажат.
Мне нужна эта информация внутри моей модели MainView. Как мне передать его туда?

РЕДАКТИРОВАТЬ ... работоспособный пример

некоторые данные для отображения в дереве:

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 (данные для элемента treeview):

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; }
  }
}

A RelayCommand :

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
  }
}

MainViewModel :

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
  }
}

ProjectTreeViewModel , где выбор был изменен распознается

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>

... и его codebehind :

using System.Windows;
using WpfApp1.ViewModels;

namespace WpfApp1
{
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainViewModel();
    }
  }
}

... после нескольких часов экспериментов я решил. Решение находится в приведенных выше фрагментах, и я не знаю, лучший ли это способ. Я сделал это следующим образом:

Командный метод ExecuteTreeNodeSelected изменяет свойство publi c SelectedNode. Это вызывает уведомление OnPropertyChanged("SelectedNode");.

При создании модели TreeView в MainViewModel я добавил прослушиватель событий к событию PropertyChanged в TreeViewModel _projectTree.PropertyChanged += ProjectTreePropertyChanged;. Это событие изменяет SelectedNode -свойство MainViewModel, которое уведомляет пользовательский интерфейс.

Спасибо за терпение

1 Ответ

0 голосов
/ 29 мая 2020

Это базовый класс ViewModel, который я использую для отображения иерархических данных в элементе управления TreeView.

/// <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. Это позволяет обнаруживать изменение выбора для средств, отличных от прямого щелчка мыши, таких как нажатие пользователем клавиш со стрелками. *

...