Какой способ WPF отметить команду как недоступную, только если родительский элемент дерева является первым в списке?

У меня есть представление дерева, представляющее определенные элементы. Это дерево всегда на два уровня. Меню правой кнопки мыши для дочерних элементов имеет команду "вверх". Пользовательский интерфейс позволяет перемещать дочерний элемент вверх, даже если его первый элемент его родителя, если на родительском уровне есть другой элемент, над родительским элементом выбранных элементов.

Очевидный способ сделать это - получить выбранные элементы parent и посмотреть, есть ли над ним элементы. Однако получение родительского элемента выбранных элементов в WPF не является чем-то большим, чем тривиальным. Опять же, очевидный (для начинающего WPF, во всяком случае) подход - получить TreeViewItem для выбранного элемента, который имеет свойство Parent. К сожалению, это также трудно сделать.

Взяв подсказку от кого-то, кто говорит что это сложно, потому что Im делает это неправильно, я решил спросить тех, кто более опытен с WPF: что это правильный, не жесткий способ сделать это? Логически его тривиально, но я не могу определить правильный способ работы с API WPF.

Ответ 1

Вы абсолютно правы, что делать подобные вещи с помощью Wpf TreeView очень болезненно. Ключевой причиной этого является гибкость, которую дает Wpf - вы могли бы переопределить ItemContainerGenerator в пользовательском TreeView, и ваше древовидное представление может фактически не содержать объекты TreeViewItem, например. то есть нет той же фиксированной иерархии, которую вы найдете в сопоставимом контроле Winforms.

На первый взгляд кажется на первый взгляд интуитивно понятным, и это настоящий позор, что MS не тратила больше времени на объяснение того, как заставить подобное работать таким образом, чтобы это не приводило к разочарованию.

Мы с огромным успехом пользовались Wpf, так как обнимали MVVM - до такой степени, что мы всегда создаем ViewModel для классов, привязанных к пользовательскому интерфейсу, без каких-либо исключений - просто гораздо проще подключиться к новой функции позже по линии.

Если у вас есть базовая модель просмотра (или даже элемент модели, если необходимо), с которой связано ваше древовидное представление, и подумайте о древовидном представлении как о наблюдателе, вы значительно улучшите работу с помощью Wpf TreeView и других элементов управления Wpf слишком. Практически для иерархии, связанной с деревом, у вас будет иерархия объектов viewmodel, которую визуализирует ваш TreeView, - где каждый ребенок имеет дескриптор назад к нему родительский, и каждый родитель имеет набор дочерних моделей просмотра. Затем у вас будет шаблон иерархических данных для каждого элемента, где ItemsSource - это ChildCollection. Затем вы удаляете команду "MoveUp" против ViewModel, и она заботится о внесении изменений - если вы используете коллекции на основе ObservableCollection (или реализуете INotifyCollectionChanged), тогда TreeView автоматически обновляет, чтобы отразить новую иерархию.

Вождение функциональности из ViewModel и просмотр пользовательского интерфейса как тонкого слоя, отражающего иерархию и свойства ViewModel, делает код, который может быть подвергнут модульной проверке в высокой степени - без кода в коде, вы можете часто протестируйте функциональность ViewModel без какого-либо пользовательского интерфейса во всем, что делает код гораздо лучшего качества в конечном итоге.

Естественный ответ для нас, когда мы начали с Wpf, заключался в том, что ViewModels были чрезмерными, но наш опыт (начавшийся без них во многих местах) заключается в том, что они начинают довольно быстро окупаться в Wpf и, без сомнения, стоят дополнительных усилий чтобы опустить голову.

Одна вещь, которую вы, возможно, еще не ударили, которую мы обнаружили очень болезненной, устанавливала выбранный элемент в виде дерева - теперь это не что-то для слабонервных:)

Ответ 2

"Правильный" способ состоял бы в том, чтобы забыть манифестацию вашей проблемы и вместо этого подумать о том, как должна выглядеть ваша модель. У вас есть модель за вашим пользовательским интерфейсом, верно?

Затем ваш пользовательский интерфейс будет просто привязываться к соответствующим свойствам вашей модели.

Ответ 3

Мне может быть что-то упущено, но я бы это сделал, передав SelectedIndex в качестве параметра команды привязке для метода CanExecute. Затем просто используйте это, чтобы решить, включена ли команда или нет.

Проблема может заключаться в том, что datacontext контекстного меню не изменяется после загрузки, потому что contextmenu isnt в визуальном дереве. Обычно я использую этот метод, чтобы выставить datacontext для элементов, не входящих в визуальное дерево, через статический ресурс. Я на самом деле написал ответ на вопрос об этом ранее сегодня.

Я действительно думаю, что мне что-то не хватает. Не могли бы вы объяснить, почему это не сработает?


Изменить

Хорошо, я прочитал abit о TreeViews и до сих пор не совсем понял, в чем проблема. Поэтому я пошел дальше, сделал пример и сумел заставить его работать.

Мой первый шаг был прочитан Эта статья Джоша Смита о древовидной структуре. В нем говорится о создании режимов просмотра для каждого типа элементов и экспонирования таких свойств, как IsSelected и IsExpanded, которые вы затем привязываете в xaml. Это позволяет вам получить доступ к свойствам treeviewitem в моделях viewmodels.

После прочтения этого я начал работать:


Во-первых, я сделал небольшую структуру данных, которая показывает какую-то иерархию, которая помещается в древовидную структуру. Я выбрал фильмы.

#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

Следующий шаг - создать модель просмотра для TreeViewItems, которая содержит все свойства, которые относятся к управлению файлами древовидного изображения, то есть IsExpanded, IsSelected и т.д.

Важно отметить, что все они имеют родителя и ребенка.

Вот как мы будем отслеживать, являемся ли мы первым или последним элементом в коллекции родителей.

 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 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 getter to return true.
    /// </summary>
    protected virtual bool ThrowOnInvalidPropertyName { get; private set; }

    #endregion // Debugging Aides
}

После этого мы создаем наши модели просмотра для каждой модели. Все они наследуются от TreeViewItemModel, поскольку все они будут отображаться в виде treeviewitems.

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



}

Затем я создал MainWindowViewModel, который создаст коллекцию этих режимов просмотра (которая привязана к TreeView), а также реализует команды, используемые в меню, и логику их включения.

Здесь важно отметить, что у меня есть свойство SelectedItem. Я получил этот элемент, подписавшись на все измененное событие свойства viewmodel, а затем получая тот, который выбран. Я использую этот элемент, чтобы проверить, является ли он первым из последнего элемента в родительской коллекции для детей.

Также обратите внимание на способ включения команды, как я решаю, находится ли элемент в корне или нет. Это важно, потому что мой mainwindowviewmodel не является TreeViewItemViewModel и не реализует свойство Children. Очевидно, что для вашей программы вам понадобится другой способ сортировки корня. Возможно, вы захотите ввести логическую переменную в TreeViewItemViewModel с именем root, для которой вы можете просто установить значение 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 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 getter to return true.
    /// </summary>
    protected virtual bool ThrowOnInvalidPropertyName { get; private set; }

    #endregion // Debugging Aides
}

Наконец, вот xaml MainWindow, где мы привязываемся к свойствам.

Обратите внимание на стиль внутри treeview для treeviewitem. Здесь мы привязываем все свойства TreeViewItem к тем, которые созданы в TreeviewItemViewModel.

Свойство contextmenu MenuItems привязано к командам через DataContextBridge (подобно ElementSpy, как творения Джоша Смита). Это связано с тем, что контекстменю находится вне визуального дерева и, следовательно, имеет проблемы с привязкой к viewmodel.

Также обратите внимание, что у меня есть другой HierarchicalDataTemplate для каждого из созданных типов viewmodel. Это позволяет мне привязываться к различным свойствам для разных типов, которые будут отображаться в древовидной структуре.

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