WPF - MVVM - TreeView - Commands enabled based on selected item - wpf

I have found many pages that bear on this in one way or another, but have still not discovered how to achieve it. Here is my XAML:
<TreeView ItemsSource="{Binding Document}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<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">
<Image Source="{Binding ImageFile}" Height="16" Width="16"/>
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
I implemented the AddCommand and DeleteCommand based roughly on the Search button implementation in this.
Both commands require the SelectedItem from the tree, so I implemented it in the tree MVVM, added a pointer to the tree MVVM to each item MVVM, and maintain it via the IsSelected property in the item MVVM.
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
}
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
}
}
}
(We use mAbc for data members, rather than _abc.)
This all works. However, the context menus have a context. Based on which is selected, the AddCommand may not be valid, and I want that represented as disabled and enabled in the view.
I put my tests for this condition in the CanExecute method of each command. But at run time, CanExecute seems never to be invoked, and both menu item always appear disabled.
Is there a way to get this done? Is there a simple way?
Thanks,
Art
LATER:
Editing my question appears to be the way to make a longer reply. Here, then, is one of the Command classes ... with respect to the CanExecute mentioned afterwards.
#region DeleteCommand
public ICommand DeleteCommand
{
get { return mDeleteCommand; }
}
void DeleteNode()
{
if (mSelectedItem != null)
{
mSelectedItem.Remove();
mSelectedItem = null;
}
}
private class DeleteNodeCommand : RoutedCommand
{
DocumentRulesViewModel mDocumentViewModel;
public DeleteNodeCommand (DocumentRulesViewModel _docViewModel)
{
mDocumentViewModel = _docViewModel;
}
void SelectedItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel current = mDocumentViewModel.SelectedItem;
return (current != null);
}
event EventHandler CanExecuteChanged
{
// I intentionally left these empty because
// this command never raises the event, and
// not using the WeakEvent pattern here can
// cause memory leaks. WeakEvent pattern is
// not simple to implement, so why bother.
add { }
remove { }
}
public void Execute(object parameter)
{
mDocumentViewModel.DeleteNode();
}
public event PropertyChangedEventHandler PropertyChanged;
}
#endregion
I didn't do anything with the event stuff at the bottom, just copied it from an example. And, in that example, the command would always be valid. So maybe the issue lies there.
But I did some prowling for CanExecuteChange, and did not really see what to do with it.
Jim, I guess all I can do it show it all (I'll have to omit the application/model parts, of course.
Main xaml:
<Window x:Class="xDesign.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:xDesign.View"
Title="{StaticResource thisAppName}" Height="350" Width="525">
<DockPanel>
<Menu VerticalAlignment="Top" DockPanel.Dock="Top" BorderThickness="0">
<MenuItem Header="{StaticResource fileMenu}" Name="FileMenu">
<MenuItem Header="{StaticResource newFileMenu}" Click="NewDocumentMenuItem_Click" Name="FileMenuNewDoc"/>
<MenuItem Header="{StaticResource openFileMenu}" Click="OpenDocumentMenuItem_Click" Name="FileMenuOpenDoc" />
<MenuItem Header="{StaticResource closeFileMenu}" Click="CloseDocumentMenuItem_Click" IsEnabled="False" Name="FileMenuCloseDoc" />
<Separator />
<MenuItem Name="FileMenuCheckout" Header="{StaticResource checkoutFileMenu}" Click="FileMenuCheckout_Click"/>
<MenuItem Name="FileMenuCheckin" Header="{StaticResource checkinFileMenu}" Click="FileMenuCheckin_Click" IsEnabled="False"/>
<MenuItem Name="FileMenuDeleteFromServer" Header="{StaticResource deleteFromServerFileMenu}" Click="FileMenuDeleteFromServer_Click" IsEnabled="False"/>
<MenuItem Name="FileMenuLogon" Header="{StaticResource logonFileMenu}" Click="FileMenuLogon_Click"/>
<MenuItem Name="FileMenuLogoff" IsEnabled="False" Header="{StaticResource logoffFileMenu}" Click="FileMenuLogoff_Click"/>
</MenuItem>
<MenuItem Header="{StaticResource editMenu}" IsEnabled="False" Name="EditMenu">
<MenuItem Header="{StaticResource findEditMenu}" Click="FindEditMenuItem_Click"/>
</MenuItem>
<MenuItem Header="{StaticResource viewMenu}" IsEnabled="False" Name="ViewMenu">
<MenuItem Header="{StaticResource expandViewMenu}" Click="ExpandViewMenuItem_Click"/>
<MenuItem Header="{StaticResource collapseViewMenu}" Click="CollapseViewMenuItem_Click"/>
</MenuItem>
</Menu>
<Grid Name="DesignPanel" DockPanel.Dock="Top">
<Grid.ColumnDefinitions >
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions >
<local:DocumentTreeView x:Name="DocTreeView" Grid.Column="0"/>
<GridSplitter Grid.Column="0" HorizontalAlignment="Right" VerticalContentAlignment="Stretch" Width="3" ResizeDirection="Columns" />
<WebBrowser x:Name="objectPreviewBrowser" Grid.Column="1" Margin="6,6,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" OpacityMask="#FF9B8E8E"/>
</Grid>
</DockPanel>
</Window>
Control xaml:
<UserControl x:Class="xDesign.View.DocumentTreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<TreeView ItemsSource="{Binding Document}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<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">
<Image Source="{Binding ImageFile}" Height="16" Width="16"/>
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add rule" Command="{Binding AddRuleCommand}"/>
<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
</UserControl>
Primary view model:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
using xDesign.Actions;
using xDesign.API.Model;
namespace xDesign.ViewModel
{
public class DocumentRulesViewModel : INotifyPropertyChanged
{
#region data members
DesignObjectViewModel mRootObject = null;
ObservableCollection<DesignObjectViewModel> mDocument = null;
DesignObjectViewModel mSelectedItem = null;
ICommand mDeleteCommand = null;
ICommand mAddRuleCommand = null;
#endregion
#region consructors
public DocumentRulesViewModel(DocumentObject _rootObject)
{
mRootObject = new DesignObjectViewModel(_rootObject, this);
mDocument = new ObservableCollection<DesignObjectViewModel>
(new DesignObjectViewModel[] { mRootObject });
mRootObject.IsExpanded = true; // We start with the top node expanded
mDeleteCommand = new DeleteNodeCommand(this);
mAddRuleCommand = new AddRuleCommandClass(this);
}
~DocumentRulesViewModel()
{
Close();
}
public void Close()
{
Document = null;
}
#endregion
#region properties
public ObservableCollection<DesignObjectViewModel> Document
{
get { return mDocument; }
set
{
if (value != mDocument)
{
mDocument = value;
this.OnPropertyChanged("Document");
}
}
}
public DesignObjectViewModel SelectedItem
{
get { return mSelectedItem; }
set
{
if (value != mSelectedItem)
{
mSelectedItem = value;
this.OnPropertyChanged("SelectedItem");
}
}
}
public IDesignObject CurrentDesignObject
{
get
{
if (mSelectedItem == null)
{
return null;
}
else
{
return mSelectedItem.DesignObject;
}
}
set
{
DesignObjectViewModel dovm = SearchForNode(value);
if (dovm != null)
{
if (dovm.Parent != null && !dovm.Parent.IsExpanded)
{
dovm.Parent.IsExpanded = true;
}
dovm.IsSelected = true;
}
}
}
#endregion
#region DeleteCommand
public ICommand DeleteCommand
{
get { return mDeleteCommand; }
}
public void DeleteItem ()
{
DesignObjectViewModel node = this.SelectedItem;
node.Remove();
}
private class DeleteNodeCommand : RoutedCommand
{
DocumentRulesViewModel mTree;
public DeleteNodeCommand(DocumentRulesViewModel _tree)
{
mTree = _tree;
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel node = mTree.SelectedItem;
return (node != null);
}
public void Execute(object parameter)
{
mTree.DeleteItem();
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
#endregion
#region AddRuleCommand
public ICommand AddRuleCommand
{
get { return mAddRuleCommand; }
}
void AddRule()
{
int index = -1; // Where to insert; -1 = inside selected item
if (mSelectedItem.Parent != null)
{
index = mSelectedItem.Parent.Children.IndexOf(mSelectedItem) + 1; // Insert after selected item
}
// Call the application logic
IDesignObject dobj = DocStructureManagement.AddRule(mSelectedItem.DesignObject, ref index);
if (dobj != null)
{
DesignObjectViewModel newItemParent;
if (index == -1)
{
newItemParent = mSelectedItem;
index = 0;
}
else
{
newItemParent = mSelectedItem.Parent;
}
DesignObjectViewModel newItem = new DesignObjectViewModel(dobj, this, newItemParent);
newItemParent.InsertChild(newItem, index);
}
}
private class AddRuleCommandClass : RoutedCommand
{
DocumentRulesViewModel mTree;
public AddRuleCommandClass(DocumentRulesViewModel _tree)
{
mTree = _tree;
}
public bool CanExecute(object parameter)
{
DesignObjectViewModel node = mTree.SelectedItem;
return (node != null && node.DesignObject.CanContainOrPrecede(eDesignNodeType.ContentRule));
}
public void Execute(object parameter)
{
mTree.AddRule();
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
#endregion
#region Search
private DesignObjectViewModel SearchForNode(IDesignObject _dobj)
{
return SearchNodeForNode(mRootObject, _dobj);
}
private DesignObjectViewModel SearchNodeForNode(DesignObjectViewModel _node, IDesignObject _dobj)
{
if (_node.DesignObject == _dobj)
{
return _node;
}
foreach (DesignObjectViewModel child in _node.Children)
{
DesignObjectViewModel childNode = SearchNodeForNode(child, _dobj);
if (childNode != null)
{
return childNode;
}
}
return null;
}
#endregion
#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
}
}
TreeViewItem view model:
using System;
using System.Collections.ObjectModel;
using System.Linq;
using xDesign.API.Model;
using xDesign.Actions;
using System.ComponentModel;
using System.Windows.Input;
namespace xDesign.ViewModel
{
public class DesignObjectViewModel : INotifyPropertyChanged
{
#region data
DocumentRulesViewModel mDocViewModel = null;
IDesignObject mDesignObject = null;
DesignObjectViewModel mParent = null;
ObservableCollection<DesignObjectViewModel> mChildren = null;
bool mIsSelected = false;
bool mIsExpanded = false;
#endregion
#region constructors
public DesignObjectViewModel(IDesignObject _dobj, DocumentRulesViewModel _docViewModel)
: this(_dobj, _docViewModel, null)
{
}
public DesignObjectViewModel(IDesignObject _dobj, DocumentRulesViewModel _docViewModel, DesignObjectViewModel _parent)
{
mDesignObject = _dobj;
mDocViewModel = _docViewModel;
mParent = _parent;
if (_dobj.Type != eDesignNodeType.ContentGroup)
{
mChildren = new ObservableCollection<DesignObjectViewModel>(
(from child in mDesignObject.Children
select new DesignObjectViewModel(child, mDocViewModel, this))
.ToList<DesignObjectViewModel>());
}
else
{
ContentHolder ch = (ContentHolder)_dobj;
mChildren = new ObservableCollection<DesignObjectViewModel>(
(from child in ch.Contents
select new DesignObjectViewModel(child, mDocViewModel, this))
.ToList<DesignObjectViewModel>());
}
}
#endregion
#region properties
public ObservableCollection<DesignObjectViewModel> Children
{
get { return mChildren; }
}
public DesignObjectViewModel Parent
{
get { return mParent; }
}
public String Name
{
get { return mDesignObject.Name; }
}
public IDesignObject DesignObject
{
get { return mDesignObject; }
}
public Type DataType
{
get { return mDesignObject.GetType(); }
}
// Can we use DataType for this, and task the View with finding a corresponding image?
// And do we want to? We could end up with file names that include Model type names.
// Better? Worse? The same?
public String ImageFile
{
get { return GetImageUri(mDesignObject); }
}
public bool IsExpanded
{
get { return mIsExpanded; }
set
{
if (value != mIsExpanded)
{
mIsExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (mIsExpanded && mParent != null)
mParent.IsExpanded = true;
}
}
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
}
CommandManager.InvalidateRequerySuggested();
}
}
}
#endregion
#region public methods
public void Remove()
{
DocStructureManagement.DeleteNode(mDesignObject); // Remove from application
if (mParent != null) // Remove from ViewModel
{
mParent.Children.Remove(this);
mParent.OnPropertyChanged("Children");
}
}
public void InsertChild(DesignObjectViewModel _newChild, int _insertIndex)
{
Children.Insert(_insertIndex, _newChild);
this.OnPropertyChanged("Children");
}
#endregion
#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
internal static string GetImageUri(IDesignObject _dobj)
{
string name = null;
switch (_dobj.Type)
{
case eDesignNodeType.Document:
name = "xDesign.ico";
break;
case eDesignNodeType.ContentRule:
name = "Content Rule.png";
break;
case eDesignNodeType.Section:
name = "section rule.png";
break;
case eDesignNodeType.Table:
name = "Table Rule.bmp";
break;
case eDesignNodeType.Read:
name = "Read Rule.bmp";
break;
case eDesignNodeType.Goto:
name = "Goto Rule.bmp";
break;
case eDesignNodeType.Label:
name = "Label Rule.bmp";
break;
case eDesignNodeType.ContentGroup:
name = "ContentGroup.png";
break;
case eDesignNodeType.Content:
name = "content.png";
break;
case eDesignNodeType.Criteria:
name = "Criteria.bmp";
break;
}
if (name == null)
{
throw new Exception("No image found for " + _dobj.Name);
}
return string.Format(#"C:\DEVPROJECTS\XDMVVM\XDMVVM\Images\{0}", name);
}
}
}
Finally, a code snippet from main window code behind, where I create and connect the main view model.
mDocumentRulesViewModel = new DocumentRulesViewModel(mCurrentDocument);
this.DocTreeView.DataContext = mDocumentRulesViewModel;
Again, I set breakpoints in the CanExecute method of each of the two command classes, and control never stops there.

I created a tiny sample project, similar to yours to solve this. I was able to have the context menu CanExecute behave correctly. If you emulate this style you will be able to solve your problem.
MainWindow.Xaml:
<Window x:Class="CommandChangesInTreeViewContextMenu.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Command="{Binding AddCommand}">Add Command </Button>
<TreeView Grid.Row="1"
ItemsSource="{Binding MasterList}" HorizontalAlignment="Stretch" BorderThickness="0" Background="#FFC2A2A2">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Margin="5,0,0,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
<!--<MenuItem Header="Delete" Command="{Binding DeleteCommand}"/>-->
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
<Button Grid.Row="2"
Command="{Binding ClearSelectionsCommand}">Clear Selections </Button>
</Grid>
</Window>
The DataContext ViewModel for the MainWindow.Xaml is TreeViewModel:
public class TreeViewModel : ObservableObject
{
private ObservableCollection<MasterItem> _masterList;
private ICommand _addCommand;
private ICommand _clearSelectionsCommand;
public ObservableCollection<MasterItem> MasterList
{
get { return _masterList; }
set
{
if (_masterList != value)
{
_masterList = value;
OnPropertyChanged("MasterList");
}
}
}
public ICommand AddCommand
{
get
{
if (_addCommand == null)
{
_addCommand = new RelayCommand<object>(Add, CanExecuteAddCommand);
}
return _addCommand;
}
}
public ICommand ClearSelectionsCommand
{
get
{
if (_clearSelectionsCommand == null)
{
_clearSelectionsCommand = new RelayCommand<object>(ClearSelections);
}
return _clearSelectionsCommand;
}
}
public TreeViewModel()
{
MasterList = new ObservableCollection<MasterItem>
{
new MasterItem("sup"), new MasterItem("hi"), new MasterItem("test"), new MasterItem("yo")
};
}
private void Add(object o)
{
// does nothing
}
private void ClearSelections(object o)
{
foreach (var mItem in MasterList)
{
mItem.IsSelected = false;
}
}
private bool CanExecuteAddCommand(object o)
{
return MasterList.Any(mItem => mItem.IsSelected == true);
}
}
The MasterItem class which are the objects in your MasterList:
MasterItem.cs:
public class MasterItem : ObservableObject
{
private string _name;
private bool _isSelected;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged("Name");
}
}
}
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
CommandManager.InvalidateRequerySuggested();
}
}
}
public MasterItem(string name)
{
Name = name;
IsSelected = false;
}
}
**Note that When IsSelected is set it will InvalidateRequerySuggested() and work properly. =) **
Supporting Classes, RelayCommand, and ObservableObject
/// <summary>
/// RelayCommand
///
/// General purpose command implementation wrapper. This is an alternative
/// to multiple command classes, it is a single class that encapsulates different
/// business logic using delegates accepted as constructor arguments.
/// </summary>
/// <typeparam name="T"></typeparam>
public class RelayCommand<T> : ICommand
{
private static bool CanExecute(T paramz)
{
return true;
}
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
/// <summary>
/// Relay Command
///
/// Stores the Action to be executed in the instance field variable. Also Stores the
/// information about IF it canexecute in the instance field variable. These executing
/// commands can be sent from other methods in other classes. Hence the lambda expressions.
/// Tries to be as generic as possible T type as parameter.
/// </summary>
/// <param name="execute">Holds the method body about what it does when it executes</param>
/// <param name="canExecute">Holds the method body conditions about what needs to happen for the ACTION
/// Execute to execute. If it fails it cannot execute. </param>
public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute ?? CanExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute(TranslateParameter(parameter));
}
// allows for constant updating if the event can execute or not.
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null)
CommandManager.RequerySuggested -= value;
}
}
public void Execute(object parameter)
{
_execute(TranslateParameter(parameter));
}
private T TranslateParameter(object parameter)
{
T value = default(T);
if (parameter != null && typeof(T).IsEnum)
value = (T)Enum.Parse(typeof(T), (string)parameter);
else
value = (T)parameter;
return value;
}
public void RaiseCanExecuteChanged()
{
// we should not have to reevaluate every can execute.
// but since there are too many places in product code to verify
// we will settle for all or nothing.
CommandManager.InvalidateRequerySuggested();
}
}
/// <summary>
/// Class is based on two delegates; one for executing the command and another for returning the validity of the command.
/// The non-generic version is just a special case for the first, in case the command has no parameter.
/// </summary>
public class RelayCommand : RelayCommand<object>
{
public RelayCommand(Action execute, Func<bool> canExecute = null)
: base(obj => execute(),
(canExecute == null ?
null : new Func<object, bool>(obj => canExecute())))
{
}
}
ObservableObject:
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, Expression<Func<T>> expression)
{
// Allows a comparison for generics. Otherwise could just say x == y ?
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
var lambda = (LambdaExpression)expression;
MemberExpression memberExpr;
if (lambda.Body is UnaryExpression)
{
var unaryExpr = (UnaryExpression)lambda.Body;
memberExpr = (MemberExpression)unaryExpr.Operand;
}
else
{
memberExpr = (MemberExpression)lambda.Body;
}
OnPropertyChanged(memberExpr.Member.Name);
return true;
}
return false;
}
}
Note that ObservableObject and RelayCommand are just helpers and not necessary to generating the solution. Mainly look at MainWindow.Xaml, TreeViewModel, and MasterItem. I hope this helps!
Picture of the disabled context menu when IsSelected is set to false for all the MasterItems in MasterList
Example of using a RelayCommand:
in your constructor
public PrimaryViewModel()
{
ICommand bob = new RelayCommand(CommandMethodThatDoesStuff,CanExecuteCommandMethod);
}
private void CommandMethodThatDoesStuff(object o)
{
// do your work
}
private bool CanExecuteCommandMethod(object o)
{
return IsSelected;
}

You are on the right track with the Commands and implementing CanExecute. I have had a similar problem. The commands will not always update their CanExecute immediately. If you want to have all of your commands update, a simple brute force solution is to add a call to CommandManager.InvalidateRequerySuggested().
public bool IsSelected
{
get { return mIsSelected; }
set
{
if (value != mIsSelected)
{
mIsSelected = value;
this.OnPropertyChanged("IsSelected");
}
if (mIsSelected)
{
mDocViewModel.SelectedItem = this;
CommandManager.InvalidateRequerySuggested();
}
}
}
This will call invalidate arrange on all of your commands and force the logic on your commands, CanExecute boolean methods to refresh their state on the UI.

There are two ways to do this, that I can think of.
1 - put the context menu on your HierarchicalDataTemplate. This means the DataContext for the context menu will be the item from the tree. This can be nice because then, for example, the AddCommand is close to where things need to be added. You don't need to track the selected item in this case.
2 - bind the IsEnabled from the MenuItem to an "IsEnabled" property in your VM, and then update it when the selection changes. This is less nice, but can be tidy. If you are only doing single selection, and you already have a property for the selected item in your VM (which you probably should have already) then you can just bind it to something like {Binding SelectedItem.IsEnabled} or something.

ContextMenus aren't part of the visible tree, so binding directly to the view models won't work. The way around it is to use a BindingProxy as explained on this page:
<TreeView ItemsSource="{Binding Items}" >
<TreeView.Resources>
<local:BindingProxy x:Key="Proxy" Data="{Binding}"/>
</TreeView.Resources>
<TreeView.ContextMenu>
<ContextMenu DataContext="{Binding Path=Data, Source={StaticResource Proxy}}">
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
Alternatively if each tree item has it's own view model then you can add the command handlers to the items themselves and bind relative to the placement target:
<TreeView ItemsSource="{Binding Items}" >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Add" Command="{Binding AddCommand}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>

Related

How to set keyboard focus to a child when its parent is made visible?

I have a ListBox with editable items. When you edit an item the first time, the edit control (a TextBox in this minimal example) initially has keyboard focus. The second time an item is edited, the TextBox does not have keyboard focus. If you test the code, items are put into edit mode by selecting them and pressing F2 or Return.
Is there any reasonable and direct way to make the TextBox always get keyboard focus when it becomes visible? Failing that, is there an unreasonable or indirect way that works reliably?
It's not feasible to use the edit template at all times, because the real edit template includes many things, such as a 300 px high ListBox with a thousand options, and a TextBox for filtering the contents of the ListBox. I tried doing this with the CellTemplate of a DevExpress GridControl, but that was a can of worms for a variety of reasons.
The reason I'm alternately showing/hiding two content controls is that when I just swap different templates into ListBox.ItemTemplate, focus is handed off to the window.
XAML:
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<ListBox
ItemsSource="{Binding Items}"
>
<ListBox.Resources>
<DataTemplate x:Key="DisplayTemplate">
<Label Content="{Binding Value}" />
</DataTemplate>
<DataTemplate x:Key="EditTemplate">
<WrapPanel FocusManager.FocusedElement="{Binding ElementName=TextBox}" Focusable="False">
<Label>Editing:</Label>
<TextBox Margin="4,2,2,2" Text="{Binding Value}" x:Name="TextBox" />
</WrapPanel>
</DataTemplate>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<ContentControl x:Name="Display" Content="{Binding}" ContentTemplate="{StaticResource DisplayTemplate}" />
<ContentControl x:Name="Edit" Content="{Binding}" ContentTemplate="{StaticResource EditTemplate}" Visibility="Collapsed" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
<Setter TargetName="Edit" Property="Visibility" Value="Visible" />
<Setter TargetName="Display" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<EventSetter Event="KeyDown" Handler="ListBoxItem_KeyDown" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
ViewModels.cs
public class ViewModel : ViewModelBase
{
public ViewModel()
{
Items = new ObservableCollection<ItemViewModel>(
new[] { "ytesadamy", "ugexudunamo", "wovaxatytol", "imuq" }.Select(s => new ItemViewModel() { Value = s }));
}
public ObservableCollection<ItemViewModel> Items { get; private set; }
}
public class ItemViewModel : ViewModelBase
{
#region Value Property
private String _value = default(String);
public String Value
{
get { return _value; }
set
{
if (value != _value)
{
_value = value;
OnPropertyChanged();
}
}
}
#endregion Value Property
#region IsEditing Property
private bool _isEditing = default(bool);
public bool IsEditing
{
get { return _isEditing; }
set
{
if (value != _isEditing)
{
_isEditing = value;
OnPropertyChanged();
}
}
}
#endregion IsEditing Property
}
#region ViewModelBase Class
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
#endregion INotifyPropertyChanged
}
#endregion ViewModelBase Class
I usually do this with a behavior:
public static class FocusOnVisibleBehavior
{
public static readonly DependencyProperty FocusProperty = DependencyProperty.RegisterAttached(
"Focus",
typeof(bool),
typeof(FocusOnVisibleBehavior),
new PropertyMetadata(false, OnFocusChange));
public static void SetFocus(DependencyObject source, bool value)
{
source.SetValue(FocusProperty, value);
}
public static bool GetFocus(DependencyObject source)
{
return (bool)source.GetValue(FocusProperty);
}
private static void OnFocusChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = d as FrameworkElement;
DependencyPropertyChangedEventHandler handler = (sender, args) =>
{
if ((bool)args.NewValue)
{
// see http://stackoverflow.com/questions/13955340/keyboard-focus-does-not-work-on-text-box-in-wpf
element.Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(delegate()
{
element.Focus(); // Set Logical Focus
Keyboard.Focus(element); // Set Keyboard Focus
//element.SelectAll();
}));
}
};
if (e.NewValue != null)
{
if ((bool)e.NewValue)
{
element.IsVisibleChanged += handler;
element.Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(delegate ()
{
element.Focus(); // Set Logical Focus
Keyboard.Focus(element); // Set Keyboard Focus
//element.SelectAll();
}));
}
else
{
element.IsVisibleChanged -= handler;
}
}
// e.OldValue is never null because it's initialized to false via the PropertyMetadata()
// Hence, the effect here is that regardless of the value that's set, we first add the
// handler and then immediately remove it.
//if (e.NewValue != null)
//{
// element.IsVisibleChanged += handler;
// element.Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(delegate ()
// {
// element.Focus(); // Set Logical Focus
// Keyboard.Focus(element); // Set Keyboard Focus
// //element.SelectAll();
// }));
//}
//if (e.OldValue != null)
// element.IsVisibleChanged -= handler;
}
Can't remember if I wrote this code myself or got it from somewhere else, either way you use it like this:
<TextBox behaviors:FocusOnVisibleBehavior.Focus="True" ... etc ... />

Bind a multiple properties of a button in WPF to a class

I'm not sure if this is possible but I'm looking for a way to bind a button to a generic class that contain all the properties i will need to use. Every button needs a relay command so that would be included but all of our buttons will need to bind visibility and being enabled. Instead of having this group of properties and relay command for every button we will use within the given windows view model I was wondering if there was a way to have the button bind to a class then in our view model we reference a new instance of that class for each button needed and then be just be able to set the properties on that class to the values we need. I hope this makes sense.
There's probably a bunch of different ways to do something like this. I don't know if I'd choose to have a class instance for each button. But here's a rough/quick/dodgy example of a solution.
The main model for the form is providing the button models by way of a list. The individual button models then handle the button bindings.
EDIT: Extended the code a bit. Now includes command bindings. Also shows use of ItemsControl as suggested by #Xavier. Hope it helps.
MainWindow.xaml:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="300" Width="400">
<StackPanel>
<!-- Known buttons -->
<StackPanel Margin="20">
<Button DataContext="{Binding ButtonModels[0], Mode=OneTime}" Content="{Binding LabelText}" Background="{Binding Colour}" Command="{Binding Command}" CommandParameter="{Binding CommandParameter}" />
<Button DataContext="{Binding ButtonModels[1], Mode=OneTime}" Content="{Binding LabelText}" Background="{Binding Colour}" Command="{Binding Command}" CommandParameter="{Binding CommandParameter}" />
<Button DataContext="{Binding ButtonModels[2], Mode=OneTime}" Content="{Binding LabelText}" Background="{Binding Colour}" Command="{Binding Command}" CommandParameter="{Binding CommandParameter}" />
</StackPanel>
<!-- Dynamic buttons -->
<StackPanel Margin="20">
<ItemsControl ItemsSource="{Binding ButtonModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding LabelText}" Background="{Binding Colour}" Command="{Binding Command}" CommandParameter="{Binding CommandParameter}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</Window>
MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new Model();
}
}
public class Model
{
private Random rnd = new Random();
public List<ButtonModel> ButtonModels { get; private set; }
public Model()
{
this.ButtonModels = new List<ButtonModel>();
for (int i = 0; i < 5; i++)
{
this.ButtonModels.Add(new ButtonModel
{
LabelText = "Button " + (i + 1),
Command = new RelayCommand((index) => { this.ChangeColour((int)index); }),
CommandParameter = i
});
}
}
private void ChangeColour(int index)
{
this.ButtonModels[index].Colour = new SolidColorBrush(Color.FromRgb((byte)rnd.Next(50, 256), (byte)rnd.Next(50, 256), (byte)rnd.Next(50, 256)));
}
}
public class ButtonModel : ObservableObject
{
private string _LabelText;
public string LabelText { get => _LabelText; set => this.SetProperty(ref _LabelText, value); }
private Brush _Colour = new SolidColorBrush(Color.FromRgb(205, 205, 205));
public Brush Colour { get => _Colour; set => this.SetProperty(ref _Colour, value); }
private RelayCommand _Command;
public RelayCommand Command { get => _Command; set => this.SetProperty(ref _Command, value); }
private int _CommandParameter;
public int CommandParameter { get => _CommandParameter; set => this.SetProperty(ref _CommandParameter, value); }
}
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (field == null && value == null)
{
return false;
}
if (field == null || !field.Equals(value))
{
field = value;
this.RaisePropertyChangedEvent(propertyName);
return true;
}
return false;
}
protected void RaisePropertyChangedEvent(string propertyName)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class RelayCommand : ICommand
{
private Action<object> execute;
private Predicate<object> canExecute;
public event EventHandler CanExecuteChanged;
public RelayCommand(Action<object> action, Predicate<object> canExecute = null)
{
this.execute = action;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return this.canExecute == null || this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
}
}

I need Wpf tree view like this

ViewModel has 2 field. Name, Childs
I need like this
1. When click on the root element, do 2 operation
first. expand yourself
second. select first child. If child element has childs, repeat 1.
otherwise do nothing
Only last child (leaf) selectable
UPDATE
Figured out a much better way to do this. This will also account for changes in the ObservableCollection.
The Xaml can just look like this
<Window.Resources>
<HierarchicalDataTemplate x:Key="Level1"
ItemsSource="{Binding Path=Childs}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
</Window.Resources>
<TreeView ItemsSource="{Binding}"
...>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
And then we handle the IsSelected Property in the Model/ViewModel instead.
public class MyViewModel : INotifyPropertyChanged
{
private static MyViewModel s_lastSelectedTestItem = null;
public MyViewModel(string name)
{
Name = name;
m_isSelected = false;
Childs = new ObservableCollection<MyViewModel>();
Childs.CollectionChanged += new NotifyCollectionChangedEventHandler(TestItems_CollectionChanged);
}
void TestItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (IsSelected == true && Childs.Count > 0)
{
Childs[0].IsSelected = true;
IsExpanded = true;
}
}
public string Name
{
get;
set;
}
public ObservableCollection<MyViewModel> Childs
{
get;
set;
}
private bool m_isSelected;
public bool IsSelected
{
get
{
return m_isSelected;
}
set
{
m_isSelected = value;
if (m_isSelected == true)
{
if (s_lastSelectedTestItem != null)
{
s_lastSelectedTestItem.IsSelected = false;
}
s_lastSelectedTestItem = this;
if (Childs.Count > 0)
{
IsExpanded = true;
Childs[0].IsSelected = true;
m_isSelected = false;
}
}
OnPropertyChanged("IsSelected");
}
}
private bool m_isExpaned;
public bool IsExpanded
{
get
{
return m_isExpaned;
}
set
{
m_isExpaned = value;
OnPropertyChanged("IsExpanded");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
What about to capture select item event on the treeview and expand the first child of the selected item? It seems easy to do.

Problem with binding a datagrid's selected item to treeview's selected value

I have problems with binding a treeview to a datagrid's selected item.
they are in different views, but datagrid's selected item is already passed to treeview's related viewmodel.
There is a SelectedGroup property in treeview's related viewmodel which is datagrid's selected item and its type is Group. I want to bind the ID field of Group to treeview, i.e. I want the ID of selected item to be selected in treeview and also be updated by selected value of treeview.
I couldn't find out how to bind.
Here's my treeview's skeleton, which can just lists all of the groups hierarchically.
Can anyone help me on filling the required fields please?
Thanks in advance.
<TreeView Grid.Column="1" Grid.Row="4" Height="251" HorizontalAlignment="Left"
Margin="4,3,0,0" Name="parentGroupTreeView" VerticalAlignment="Top"
Width="246" ItemsSource="{Binding Groups}" ItemContainerStyle="{x:Null}"
SelectedValuePath="ID">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding
Converter={x:Static Member=conv:GroupSubGroupsConv.Default}}">
<Label Name="groupLabel" Content="{Binding GroupName}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Start by taking a look at the following article by Josh Smith on Simplifying the WPF TreeView by Using the ViewModel Pattern.
I am also using the DataGrid from the WPF toolkit.
To get a sense as to how this code works look at the IsSelected property below.
Here is the XAML that contains a tree and a datagrid:
<Window x:Class="TreeviewDatagrid.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfToolkit="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
xmlns:ViewModels="clr-namespace:TreeviewDatagrid.ViewModels" Title="Main Window" Height="400" Width="800">
<DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="7*"/>
</Grid.ColumnDefinitions>
<TreeView ItemsSource="{Binding Groups}"
Grid.Column="0">
<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.Resources>
<HierarchicalDataTemplate
DataType="{x:Type ViewModels:GroupViewModel}"
ItemsSource="{Binding Children}" >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding GroupName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
<WpfToolkit:DataGrid
Grid.Column="1"
SelectedItem="{Binding Path=SelectedGroup, Mode=TwoWay}"
ItemsSource="{Binding Path=Groups, Mode=OneWay}" >
</WpfToolkit:DataGrid>
</Grid>
</DockPanel>
</Window>
Here is the main view model that the TreeView and DataGrid bind to:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using TreeviewDatagrid.Models;
namespace TreeviewDatagrid.ViewModels
{
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
Group g1 = new Group();
g1.Id = 1;
g1.GroupName = "Planners";
g1.Description = "People who plan";
GroupViewModel gvm1 = new GroupViewModel(this, g1);
Group g2 = new Group();
g2.Id = 2;
g2.GroupName = "Thinkers";
g2.Description = "People who think";
GroupViewModel gvm2 = new GroupViewModel(this, g2);
Group g3 = new Group();
g3.Id = 3;
g3.GroupName = "Doers";
g3.Description = "People who do";
GroupViewModel gvm3 = new GroupViewModel(this, g3);
IList<GroupViewModel> list = new List<GroupViewModel>();
list.Add(gvm1);
list.Add(gvm2);
list.Add(gvm3);
_selectedGroup = gvm1;
_groups = new ReadOnlyCollection<GroupViewModel>(list);
}
readonly ReadOnlyCollection<GroupViewModel> _groups;
public ReadOnlyCollection<GroupViewModel> Groups
{
get { return _groups; }
}
private GroupViewModel _selectedGroup;
public GroupViewModel SelectedGroup
{
get
{
return _selectedGroup;
}
set
{
// keep selection in grid in-sync with tree
_selectedGroup.IsSelected = false;
_selectedGroup = value;
_selectedGroup.IsSelected = true;
OnPropertyChanged("SelectedGroup");
}
}
public void ChangeSelectedGroup(GroupViewModel selectedGroup)
{
_selectedGroup = selectedGroup;
OnPropertyChanged("SelectedGroup");
}
}
}
Here is the viewmodel that I use to bind to the grid and the tree:
using TreeviewDatagrid.Models;
namespace TreeviewDatagrid.ViewModels
{
public class GroupViewModel : TreeViewItemViewModel
{
private readonly MainViewModel _mainViewModel;
readonly Group _group;
bool _isSelected;
public GroupViewModel(MainViewModel mainViewModel, Group group) : base(null, true)
{
_mainViewModel = mainViewModel;
_group = group;
}
public string GroupName
{
get { return _group.GroupName; }
}
public override bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
if (_isSelected )
{
// keep tree selection in sync with grid
_mainViewModel.ChangeSelectedGroup(this);
}
this.OnPropertyChanged("IsSelected");
}
}
}
protected override void LoadChildren()
{
// load children in treeview here
}
}
}
For completeness here is the Group object:
namespace TreeviewDatagrid.Models
{
public class Group
{
public int Id { get; set; }
public string GroupName { get; set; }
public string Description { get; set; }
}
}
And also the base class for the TreeView:
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace TreeviewDatagrid.ViewModels
{
/// <summary>
/// Base class for all ViewModel classes displayed by TreeViewItems.
/// This acts as an adapter between a raw data object and a TreeViewItem.
/// </summary>
public class TreeViewItemViewModel : INotifyPropertyChanged
{
#region Data
static readonly TreeViewItemViewModel DummyChild = new TreeViewItemViewModel();
readonly ObservableCollection<TreeViewItemViewModel> _children;
readonly TreeViewItemViewModel _parent;
bool _isExpanded;
bool _isSelected;
#endregion // Data
#region Constructors
protected TreeViewItemViewModel(TreeViewItemViewModel parent, bool lazyLoadChildren)
{
_parent = parent;
_children = new ObservableCollection<TreeViewItemViewModel>();
if (lazyLoadChildren)
_children.Add(DummyChild);
}
// This is used to create the DummyChild instance.
private TreeViewItemViewModel()
{
}
#endregion // Constructors
#region Presentation Members
#region Children
/// <summary>
/// Returns the logical child items of this object.
/// </summary>
public ObservableCollection<TreeViewItemViewModel> Children
{
get { return _children; }
}
#endregion // Children
#region HasLoadedChildren
/// <summary>
/// Returns true if this object's Children have not yet been populated.
/// </summary>
public bool HasDummyChild
{
get { return this.Children.Count == 1 && this.Children[0] == DummyChild; }
}
#endregion // HasLoadedChildren
#region IsExpanded
/// <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");
}
// Expand all the way up to the root.
if (_isExpanded && _parent != null)
_parent.IsExpanded = true;
// Lazy load the child items, if necessary.
if (this.HasDummyChild)
{
this.Children.Remove(DummyChild);
this.LoadChildren();
}
}
}
#endregion // IsExpanded
#region IsSelected
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is selected.
/// </summary>
public virtual bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
}
}
#endregion // IsSelected
#region LoadChildren
/// <summary>
/// Invoked when the child items need to be loaded on demand.
/// Subclasses can override this to populate the Children collection.
/// </summary>
protected virtual void LoadChildren()
{
}
#endregion // LoadChildren
#region Parent
public TreeViewItemViewModel 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
}
}

Is there a way to get the state of which TreeView nodes are expanded/collapsed?

This code example shows how to get the position of the scrollbar in a ScrollViewer (with ScrollToVerticalOffset) so that e.g. you can reset the scrollbar to this position if you need to recreate it.
Is there any way to do this for a TreeView control, i.e. get a collection of node indexes which the user has expanded?
The easiest way to do this is to bind TreeViewItem.IsExpanded property to the ViewModel, and then go through model and calculate.
I wrote an example for you. It calculates number of expanded nodes, but you can do whatever you want with expanded guys...
C#:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace WpfApplication1
{
public partial class TestBrowser : Window
{
private TreeViewItemViewModel[] _items;
public TestBrowser()
{
InitializeComponent();
var item1 = new TreeViewItemViewModel();
var item2 = new TreeViewItemViewModel();
var item3 = new TreeViewItemViewModel();
item3.Children.Add(new TreeViewItemViewModel());
item3.Children.Add(new TreeViewItemViewModel());
var child3 = new TreeViewItemViewModel();
child3.Children.Add(new TreeViewItemViewModel());
item3.Children.Add(child3);
_items = new[] {item1, item2, item3};
tv.DataContext = _items;
}
private void CalculateExpandedClick(object sender, RoutedEventArgs e)
{
var expanded = 0;
foreach (TreeViewItemViewModel item in _items)
{
expanded += GetNumberOfExpanded(item);
}
ExpandedNumber.Text = expanded.ToString();
}
private int GetNumberOfExpanded(TreeViewItemViewModel model)
{
var expandedCount = 0;
if (model.IsExpanded)
{
expandedCount += 1;
foreach (TreeViewItemViewModel child in model.Children)
{
expandedCount += GetNumberOfExpanded(child);
}
}
return expandedCount;
}
}
/// <summary>
/// Single tree view item view model.
/// </summary>
public class TreeViewItemViewModel : INotifyPropertyChanged
{
public ObservableCollection<TreeViewItemViewModel> Children
{
get; private set;
}
private bool _isExpanded;
private string _text;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
_isExpanded = value;
OnPropertyChanged("IsExpanded");
}
}
public string Text
{
get { return _text; }
set
{
_text = value;
OnPropertyChanged("Text");
}
}
public TreeViewItemViewModel()
{
Children = new ObservableCollection<TreeViewItemViewModel>();
Text = DateTime.Now.ToLongTimeString(); // Just fake data.
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string name)
{
var changed = PropertyChanged;
if (changed != null)
{
changed(this, new PropertyChangedEventArgs(name));
}
}
}
}
XAML:
<Window x:Class="WpfApplication1.TestBrowser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Expanded Test"
Height="300"
Width="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TreeView x:Name="tv"
ItemsSource="{Binding}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Text}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<StackPanel Grid.Column="1">
<Button Content="Get number of expanded items"
Click="CalculateExpandedClick"/>
<TextBlock x:Name="ExpandedNumber" />
</StackPanel>
</Grid>
</Window>
Hope this helps.

Resources