Какой способ WPF пометить команду как недоступную, только если родительский элемент дерева является первым в списке? - PullRequest
2 голосов
/ 18 марта 2012

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

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

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

Ответы [ 3 ]

5 голосов
/ 19 марта 2012

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

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

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

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

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

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

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

1 голос
/ 19 марта 2012

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

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

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


Редактировать

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

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

Прочитав это, я принялся за работу:


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

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

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

  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. Я получил этот элемент, подписавшись на all событие изменения свойства viewmodel, а затем получив выбранное. Я использую этот предмет, чтобы проверить, является ли он первым из последнего предмета в его родительской коллекции Children.

Также обратите внимание, что в команде, включающей методы, я решаю, находится ли элемент в корне или нет. Это важно, потому что моя 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'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
}

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

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

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

Также обратите внимание, что у меня есть разные 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>
1 голос
/ 19 марта 2012

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...