Looking for Prism example of Modules loading themselves into a menu - wpf

Does anyone know of WPF code examples using Prism in which modules each register themselves as a menuitem in a menu within another module?
(I've currently got an application which tries to do this with the EventAggregator, so one module listens for published events from other modules which need to have their title in the menu as a menu item, but I'm getting problems with the order of loading and threading etc. I want to find an example that uses classic Prism structure to do this.)
I'm thinking in terms of this:
Shell.xaml:
<DockPanel>
<TextBlock Text="Menu:" DockPanel.Dock="Top"/>
<Menu
Name="MenuRegion"
cal:RegionManager.RegionName="MenuRegion"
DockPanel.Dock="Top"/>
</DockPanel>
Contracts View:
<UserControl x:Class="ContractModule.Views.AllContracts"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<MenuItem Header="Contracts">
</MenuItem>
</UserControl>
Customers View:
<UserControl x:Class="CustomerModule.Views.CustomerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<MenuItem Header="Customers">
</MenuItem>
</UserControl>
But up to know I've done non-Prism MVVM application structure and Menus were always nicely bound to ObservableCollections in the ViewModel and the above seems to break this nice pattern. Is the above the customary way to do it in Prism?

Update:
I created a sample for you. It's here: Sample (now dead link)
It's got a few things you have probably not thought of yet, like a contract that will allow your modules to control your shell (so you can do stuff like Open Window, that kind of thing). It is designed with MVVM in mind. I don't know if you are using that, but I would consider it.
I tried for a few minutes to get the tab titles correct, but I ended up leaving off with "A Tab". It's left as an exercise for you if you go with a tabbed UI. I've designed it to be lookless, so you can replace the XAML in the Shell.xaml without breaking anything. That's one of the advantages to the RegionManager stuff if you use it right.
Anyway, good luck!
I've never seen an example of this, but you'd have to implement this yourself.
You'd have to create your own interface, something like this:
public interface IMenuRegistry
{
void RegisterViewWithMenu(string MenuItemTitle, System.Type viewType);
}
Your Modules then would declare a dependency on an IMenuRegistry and register their views.
Your implementation of IMenuRegistry (which you would likely implement and register in the same project that hosts your Bootstrapper) you would add those menu items to your menu or treeview or whatever you are using for your menu.
When a user clicks on an item you will have to use your Bootstrapper.Container.Resolve(viewType) method to create an instance of the view and stuff it in whatever placeholder you want to show it in.

I am using MEF along with prism 6.0 and MVVM
1.Create a Menuviewmodel class for Leafmenu and TopLevel MenuViewmodel class for Toplevel menu. Menuviewmodel class will have all the properties you want to bind your menu with. Moduleui implementing this interafce must have an attribute like this
[Export(typeof(IMenu))]
public class MenuViewModel:ViewModelBase
{
public String Name { get; private set; }
public UIMenuOptions ParentMenu { get; private set; }
private bool _IsToolTipEnabled;
public bool IsToolTipEnabled
{
get
{
return _IsToolTipEnabled;
}
set
{
SetField(ref _IsToolTipEnabled, value);
}
}
private String _ToolTipMessage;
public String ToolTipMessage
{
get
{
return _ToolTipMessage;
}
set
{
SetField(ref _ToolTipMessage, value);
}
}
private IExtensionView extensionView;
public MenuViewModel(String name, UIMenuOptions parentmenu,
bool isMenuCheckable = false,
IExtensionView extensionView =null)
{
if(name.Contains('_'))
{
name= name.Replace('_', ' ');
}
name = "_" + name;
this.Name = name;
this.ParentMenu = parentmenu;
this.IsMenuCheckable = isMenuCheckable;
this.extensionView = extensionView ;
}
private RelayCommand<object> _OpenMenuCommand;
public ObservableCollection<MenuViewModel> MenuItems { get; set; }
public ICommand OpenMenuCommand
{
get
{
if(_OpenMenuCommand==null)
{
_OpenMenuCommand = new RelayCommand<object>((args =>
OpenMenu(null)));
}
return _OpenMenuCommand;
}
}
private void OpenMenu(object p)
{
if (extensionView != null)
{
extensionView .Show();
}
}
private bool _IsMenuEnabled=true;
public bool IsMenuEnabled
{
get
{
return _IsMenuEnabled;
}
set
{
SetField(ref _IsMenuEnabled, value);
}
}
public bool IsMenuCheckable
{
get;
private set;
}
private bool _IsMenuChecked;
public bool IsMenuChecked
{
get
{
return _IsMenuChecked;
}
set
{
SetField(ref _IsMenuChecked, value);
}
}
}
public class ToplevelMenuViewModel:ViewModelBase
{
public ObservableCollection<MenuViewModel> ChildMenuViewModels {
get; private set; }
public String Header { get; private set; }
public ToplevelMenuViewModel(String header,
IEnumerable<MenuViewModel> childs)
{
this.Header ="_"+ header;
this.ChildMenuViewModels =new
ObservableCollection<MenuViewModel>(childs);
}
}
}
Create an IMenu Interface wich has MenuViewModel property
public interface IMenu
{
MenuViewModel ExtensionMenuViewModel
{
get;
}
}
3.You need to implement IMenu Interface in ModuleUi of all your modules which will get loaded into a menu.
4.Implement MefBootstrapper
5.Override Configure aggregate catalog method
6.To the catalog add diretory catalog containing all your module dlls, IMenu interface dll.Code is below
protected override void ConfigureAggregateCatalog()
{
base.ConfigureAggregateCatalog();
AggregateCatalog.Catalogs.Add(new
AssemblyCatalog(typeof(Bootstrapper).Assembly));
AggregateCatalog.Catalogs.Add(new
AssemblyCatalog(typeof(IMenu).Assembly));
//create a directorycatalog with path of a directory conatining
//your module dlls
DirectoryCatalog dc = new DirectoryCatalog(#".\Extensions");
AggregateCatalog.Catalogs.Add(dc);
}
in your main project add refence to IMenu interafce dll
8.In mainwindow.xaml.cs class declare a property
public ObservableCollection ClientMenuViewModels
{ get; private set; }
declare a private field
private IEnumerable<IMenu> menuExtensions;
In your mainwindow or shell constructor
[ImportingConstructor]
public MainWindow([ImportMany] IEnumerable<IMenu> menuExtensions)
{
this.menuExtensions = menuExtensions;
this.DataContext=this;
}
private void InitalizeMenuAndOwners()
{
if (ClientMenuViewModels == null)
{
ClientMenuViewModels = new
ObservableCollection<ToplevelMenuViewModel>();
}
else
{
ClientMenuViewModels.Clear();
}
if (menuExtensions != null)
{
var groupings = menuExtensions.Select
(mnuext => mnuext.ClientMenuViewModel).GroupBy(mvvm =>
mvvm.ParentMenu);
foreach (IGrouping<UIMenuOptions, MenuViewModel> grouping in
groupings)
{
UIMenuOptions parentMenuName = grouping.Key;
ToplevelMenuViewModel parentMenuVM = new
ToplevelMenuViewModel(
parentMenuName.ToString(),
grouping.Select(grp => { return (MenuViewModel)grp; }));
ClientMenuViewModels.Add(parentMenuVM);
}
}}
}
In your Shell.xaml or Mainwindow.xaml define a menu region and bind the itemssource property to ClientMenuViewModels
<Menu HorizontalAlignment="Left"
Background="#FF0096D6"
Foreground="{StaticResource menuItemForegroundBrush}"
ItemsSource="{Binding ClientMenuViewModels}"
TabIndex="3">
<Menu.Resources>
<Style x:Key="subMneuStyle" TargetType="{x:Type MenuItem}">
<Setter Property="Foreground" Value="#FF0096D6" />
<Setter Property="FontFamily" Value="HP Simplified" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Background" Value="White" />
<Setter Property="Command" Value="{Binding
OpenMenuCommand}" />
<Setter Property="IsCheckable" Value="{Binding
IsMenuCheckable}" />
<Setter Property="IsChecked" Value="{Binding
IsMenuChecked, Mode=TwoWay}" />
<Setter Property="IsEnabled" Value="{Binding
IsMenuEnabled, Mode=TwoWay}" />
<Setter Property="ToolTip" Value="{Binding
ToolTipMessage, Mode=OneWay}" />
<Setter Property="ToolTipService.ShowOnDisabled" Value="
{Binding IsToolTipEnabled, Mode=OneWay}" />
<Setter Property="ToolTipService.IsEnabled" Value="
{Binding IsToolTipEnabled, Mode=OneWay}" />
<Setter Property="ToolTipService.ShowDuration"
Value="3000" />
<Setter Property="ToolTipService.InitialShowDelay"
Value="10" />
</Style>
<my:MyStyleSelector x:Key="styleSelector" ChildMenuStyle="
{StaticResource subMneuStyle}" />
<HierarchicalDataTemplate DataType="{x:Type
plugins:ToplevelMenuViewModel}"
ItemContainerStyleSelector="{StaticResource styleSelector}"
ItemsSource="{Binding ChildMenuViewModels}">
<Label Margin="0,-5,0,0"
Content="{Binding Header}"
FontFamily="HP Simplified"
FontSize="12"
Foreground="{StaticResource menuItemForegroundBrush}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type plugins:MenuViewModel}">
<Label VerticalContentAlignment="Center"
Content="{Binding Name}"
Foreground="#FF0096D6" />
</DataTemplate>
</Menu.Resources>
<Menu.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Menu.ItemsPanel>
</Menu>
public class MyStyleSelector : StyleSelector
{
public Style ChildMenuStyle { get; set; }
public Style TopLevelMenuItemStyle { get; set; }
public override Style SelectStyle(object item, DependencyObject
container)
{
if (item is MenuViewModel)
{
return ChildMenuStyle;
}
//if(item is ToplevelMenuViewModel)
//{
// return TopLevelMenuItemStyle;
//}
return null;
}
}
here is ViewModelBase class
public class ViewModelBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler =Volatile.Read(ref PropertyChanged);
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
};
}
protected bool SetField<T>(ref T field, T value,[CallerMemberName] string propertyName="")
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
RelayCommand class is below
public class RelayCommand<T> : ICommand
{
#region Fields
private readonly Action<T> _execute = null;
private readonly Predicate<T> _canExecute = null;
#endregion
#region Constructors
/// <summary>
/// Creates a new command that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public RelayCommand(Action<T> execute)
: this(execute, null)
{
}
/// <summary>
/// Creates a new command with conditional execution.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action<T> execute, Predicate<T> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion
#region ICommand Members
/// <summary>
/// Defines the method that determines whether the command can execute in its current state.
/// </summary>
/// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
/// <returns>
/// true if this command can be executed; otherwise, false.
/// </returns>
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}
public event EventHandler CanExecuteChanged
{
add
{
if (_canExecute != null)
CommandManager.RequerySuggested += value;
}
remove
{
if (_canExecute != null)
CommandManager.RequerySuggested -= value;
}
}
public void Execute(object parameter)
{
_execute((T)parameter);
}
#endregion
}

Related

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

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>

MVVM WPF Command not firing

I have MVVM WPF application. I create Menu via a separate service and set to load time.
My shell view which contains the menu is as follows:
<ItemsControl x:Name="MainToolbar" cal:RegionManager.RegionName="{x:Static inf:RegionNames.MainToolBarRegion}" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="1,1,1,1" RenderTransformOrigin="-0.133,-5.917" Height="28" >
<ItemsControl.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</ItemsControl.RenderTransform>
<Menu IsMainMenu="True" Margin="0,0,0,0" Height="28" ItemsSource="{Binding Path=Menu}" Width="400">
<Menu.Resources>
<Style x:Key="ThemeMenuItemStyle" TargetType="MenuItem" BasedOn="{StaticResource KV_MenuItem}">
<Setter Property="Header" Value="{Binding Path=Text}"></Setter>
<Setter Property="Command" Value="{Binding Path=Command}"/>
<Setter Property="CommandParameter" Value="{Binding Path=Text}"/>
<Setter Property="IsCheckable" Value="True"/>
<Setter Property="MinWidth" Value="80"/>
</Style>
</Menu.Resources>
</Menu>
</ItemsControl>
MenuItem has a view model behind:
public class MenuItemViewModel : NotificationObject
{
private string text;
private ICommand command;
private IList<MenuItemViewModel> menuItems;
public MenuItemViewModel()
{
this.menuItems = new List<MenuItemViewModel>();
}
public string Text
{
get
{
return text;
}
set
{
if (text != value)
{
text = value;
RaisePropertyChanged(() => this.Text);
}
}
}
public ICommand Command
{
get
{
return command;
}
set
{
if (command != value)
{
command = value;
RaisePropertyChanged(() => this.Command);
}
}
}
public IList<MenuItemViewModel> MenuItems
{
get { return menuItems; }
set
{
if (menuItems != value)
{
menuItems = value;
RaisePropertyChanged(() => this.MenuItems);
}
}
}
}
My Relay command:
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
/// <summary>
/// Creates a new command that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
/// <summary>
/// Creates a new command.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
In menu service, I assign the command object like follows:
...
MenuItemViewModel app = new MenuItemViewModel();
app.Text = "APP";
app.Command = new RelayCommand(
param => this.App(""),
param => true);
MenuItemViewModel exit = new MenuItemViewModel();
exit.Text = "Exit";
exit.Command = new RelayCommand(
param => this.Exit(""),
param => this.CanExit
);
app.MenuItems.Add(exit);
mService.AddMenu(app);
...
My MenuItem is loading fine with assigned header text, but when I click a menu item it is not firing the relevant delegate methods such App("") or Exit("")
What I am doing wrong?

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?

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 a child item up even if it’s the first item of its parent, as long as there is another item at the parent level, above the selected item’s parent.
The obvious way to do this is to get the selected item’s parent and see if there are items above it. However, getting the selected item’s parent in WPF is anything but trivial. Again, the obvious (for a WPF beginner, anyway) approach is to get the TreeViewItem for the selected item, which has a Parent property. Unfortunately, this is also hard to do.
Taking the hint from someone who says it’s hard because I’m doing it wrong, I decided to ask those more experienced with WPF: what’s the right, non-hard way to do this? Logically it’s trivial, but I can’t figure out the correct way to deal with the WPF APIs.
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 :)
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.
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>

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

WPF datatemplate command

I've got a datatemplate for a tabcontrol's itemtemplate as follows;
<DataTemplate x:Key="TabItemTemplate">
<DockPanel Width="120">
<Button
Command="{Binding Path=DataContext.DeleteTimeTableCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}"
Content="X"
Cursor="Hand"
DockPanel.Dock="Right"
Focusable="False"
Margin="0,1,0,0"
Padding="0"
VerticalContentAlignment="Bottom"
Width="16" Height="16" />
This is OK as it gives me a button in the tabcontrol to allow for deleting the current tabitem.
Trouble I'm having is that the Delete command I'm binding to has a canExecute method which updates all buttons across all of the tabs in the tabcontrol. I just want the current tab to be affected.
I've got property CanDelete which I want to include in my Command. I'm trying to find a good example on CommandParameters as I think this is the way I need to go.
Has anyone got a good suggestion for the best way to do this?
Thanks.
I doubt that you still need help with this, but figured I'd take a crack at answering it anyway.
The way that I have done it in the past is to make the collection of items that you are binding to your TabControl be a collection of simple ViewModel objects. That way you can implement the CanXXX logic for each one of the tabs instead of the TabControl or view as a whole.
In this example, I am using the RelayCommand class that is shown in the Josh Smith's MVVM article.
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
namespace TabBinding.ViewModels
{
class MainViewModel : ViewModelBase
{
private ObservableCollection<TabViewModel> _Tabs;
public ObservableCollection<TabViewModel> Tabs
{
get { return _Tabs; }
set
{
_Tabs = value;
OnPropertyChanged(this, "Tabs");
}
}
public MainViewModel()
{
var tabs = new ObservableCollection<TabViewModel>();
tabs.Add(new TabViewModel() { TabHeader = "Tab1", Content="Content For Tab1" });
tabs.Add(new TabViewModel() { TabHeader = "Tab2", Content = "Content For Tab2" });
tabs.Add(new TabViewModel() { TabHeader = "Tab3", Content = "Content For Tab3" });
tabs.Add(new TabViewModel() { TabHeader = "Tab4", Content = "Content For Tab4" });
Tabs = tabs;
}
}
}
TabViewModel.cs
using System.Windows.Input;
using System.Windows;
namespace TabBinding.ViewModels
{
class TabViewModel : ViewModelBase
{
RelayCommand _CloseTabCommand;
private string _TabHeader;
public string TabHeader
{
get { return _TabHeader; }
set
{
_TabHeader = value;
OnPropertyChanged(this, "TabHeader");
}
}
private string _Content;
public string Content
{
get { return _Content; }
set
{
_Content = value;
OnPropertyChanged(this, "Content");
}
}
public ICommand CloseTabCommand
{
get
{
if (_CloseTabCommand == null)
{
_CloseTabCommand = new RelayCommand(
param => this.CloseTab(),
param => this.CanCloseTab
);
}
return _CloseTabCommand;
}
}
public void CloseTab()
{
MessageBox.Show("Close Me!");
}
bool CanCloseTab
{
get { return (TabHeader == "Tab2" || TabHeader == "Tab4"); }
}
}
}
ViewModelBase.cs
using System.ComponentModel;
namespace TabBinding.ViewModels
{
class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(object sender, string propertyName)
{
if (this.PropertyChanged != null)
{
PropertyChanged(sender, new PropertyChangedEventArgs(propertyName));
}
}
}
}
RelayCommand.cs
using System;
using System.Diagnostics;
using System.Windows.Input;
namespace TabBinding
{
/// <summary>
/// A command whose sole purpose is to
/// relay its functionality to other
/// objects by invoking delegates. The
/// default return value for the CanExecute
/// method is 'true'.
/// </summary>
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
/// <summary>
/// Creates a new command that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
/// <summary>
/// Creates a new command.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
[DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
}
MainWindow.xaml
<Window x:Class="TabBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:TabBinding.ViewModels"
Title="MainWindow" Height="360" Width="550">
<Window.Resources>
<vm:MainViewModel x:Key="Data" />
</Window.Resources>
<Grid DataContext="{StaticResource Data}">
<TabControl
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="10,10,10,10"
Width="500"
Height="300"
ItemsSource="{Binding Tabs}">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="X" Margin="0,0,10,0" Command="{Binding CloseTabCommand}" />
<TextBlock Text="{Binding TabHeader}"/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="Content" Value="{Binding Content}"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</Grid>
</Window>
App.xaml
<Application x:Class="TabBinding.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Views/MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
If anyone is still interested about the answer, you can use the CommandParameter binding extension to pass the current model.
<Button Command="{Binding Path=DataContext.DeleteTimeTableCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}" CommandParameter="{Binding}" />
The passed object is going to be the DataContext of the tab item. The solution requires the ICommand implementation to handle the given parameter properly (casting etc). Furthermore, the RequerySuggested event should be raised after any modification on the model, since WPF cannot figure out when to requery the CanExecute methods on the tabs. Another thing to keep in mind when using asynchron programming models is to raise the refresh event from the UI thread only. Nothing is going to happen otherwise.

Resources