What’s the WPF way to mark a command as unavailable only if the parent of a tree item is the first in the list?

前端 未结 3 1552
粉色の甜心
粉色の甜心 2021-01-22 13:28

I have a tree view representing certain items. This tree is always two levels deep. The right-click menu for the child items has a \"move up\" command. The UI allows you to move

相关标签:
3条回答
  • 2021-01-22 13:44

    The "right" way would be to forget the UI manifestation of your problem and instead think about how your model should represent it. You do have a model behind your UI, right?

    Your UI would then just bind to the appropriate properties on your model.

    0 讨论(0)
  • 2021-01-22 13:46

    You are absolutely right that doing this kind of thing with the Wpf TreeView is painful. A key part of the reason for that is the flexibility that Wpf gives you - you could have overriden the ItemContainerGenerator in a custom TreeView and your tree view might not actually contain TreeViewItem objects, for example. i.e. there isn't that same fixed hierarchy that you find in a comparable Winforms control.

    It really seems counter intuitive at first and it's a real shame that MS didn't spend more time explaining how to make this kind of thing work in a way that doesn't lead to frustration.

    We've had huge success with Wpf since embracing MVVM - to the point where we always create a ViewModel for classes bound to the UI, without exception - it's just that much easier to wire in new functionality later down the line.

    If you have an underlying viewmodel (or even model item if you must) that your tree view is bound to, and think of the treeview as just an observer, you will get along much better with the Wpf TreeView and other Wpf controls too. In practical terms for a tree bound hierarchy, you would have a hierarchy of viewmodel objects that your TreeView is visualizing - where each child has a handle back to it's parent, and each parent has a collection of child viewmodels. You would then have Hierarchical data template for each item, where the ItemsSource is the ChildCollection. You then fire off your "MoveUp" command against the ViewModel, and it takes care of making the change - if you are using collections based on ObservableCollection (or that implement INotifyCollectionChanged) then the TreeView updates automagically to reflect the new hierarchy.

    Driving the functionality from the ViewModel, and seeing the UI as just a thin layer reflecting the ViewModel hierarchy and properties makes for code that can be unit tested to a high degree - with no code in the code-behind, you can often test your ViewModel functionality without any UI at all which makes for much better quality code in the long run.

    The natural response for us when we started with Wpf was that ViewModels were overkill, but our experience (having started off without them in many places) is that they start paying off pretty rapidly in Wpf and are without doubt worth the extra effort to get your head around.

    One thing you might not have hit yet, which we found really painful, was setting the selected item on a treeview - now that's not something for the faint of heart :)

    0 讨论(0)
  • 2021-01-22 13:54

    I may be missing something here, but what I would do is pass the SelectedIndex as a command parameter to the binding for the command's CanExecute method. Then just use that to decide whether the command is enabled or not.

    The problem may be that datacontext of the context menu doesnt change after loading because the contextmenu isnt in the visual tree. I usually use this method to expose the datacontext to items not in the visual tree via a static resource. I actually wrote an answer to a question about this earlier today.

    I really think I'm missing something. Could you explain why this wouldn't work?


    Edit

    Ok I read abit about TreeViews and still didn't really understand what the issue was. So I went ahead and made an example and managed to get it to work.

    My first step was reading This article by Josh Smith about treeviews. It talks about making viewmodels for each item type and exposing properties like IsSelected and IsExpanded, which you then bind to in the xaml. This allows you to access properties of the treeviewitem in the viewmodels.

    After reading this I set to work:


    Firstly I made a small datastructure which shows some kind of hierarchy to put into the tree view. I picked movies.

    #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
    

    Next Step is to create a viewmodel for the TreeViewItems, which holds all the properties that relate to managing the tree view stuff i.e. IsExpanded, IsSelected etc.

    The important thing to note is that they all have a parent and child.

    This is how we are going to keep track of whether we are the first or last item in the parents collection.

     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
    }
    

    After this we create our viewmodels for each Model. They all inherit from TreeViewItemModel, as they are all going to be 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");
            }
        }
    
    
    
    }
    

    Then I created the MainWindowViewModel, which will create a collection of these viewmodels (which is bound to the TreeView) as well as implement the commands the menus use, and the logic for how they are enabled.

    It is important to note here that I have a SelectedItem property. I got this item by subscribing to all the viewmodel's property changed event and then getting the one that is selected. I use this item to check whether it is the first of last item in its parents Children collection.

    Also note in the command enabling methods how I decide whether the item is in the root or not. This is important because my mainwindowviewmodel is not a TreeViewItemViewModel, and does not implement a Children property. Obviously for your program you will need another way of sorting out the root. You may want to put a boolean variable in the TreeViewItemViewModel called root, which you can just set to true if the item has no parent.

     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
    }
    

    Lastly, here is the xaml of the MainWindow, where we bind to the properties.

    Note the style inside the treeview for the treeviewitem. This is where we bind all the TreeViewItem properties to those created in the TreeviewItemViewModel.

    The contextmenu's MenuItems's command property is bound to the commands, via a DataContextBridge (similar to an ElementSpy, both Josh Smith creations). This is because the contextmenu is out of the visual tree and therefore has trouble binding to the viewmodel.

    Also note that I have a different HierarchicalDataTemplate for each of the viewmodel types I created. This allows me to bind to different properties for the different types that will be displayed in the treeview.

     <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>
    
    0 讨论(0)
提交回复
热议问题