I constructed a treeView WPF MVVM with the help of this very good article
Then I created a contextMenu for some node that allowed me to add children from selected parent.
The problem is if I click on "Add" without expanding manually the selected node(parent), a strange child is created automatically in addition to the node expected to be generated when clicking on "Add".
I tried to detect the problem so I change the code below from:
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
to:
<Setter Property="IsExpanded" Value="True" />
Image 1 below shows the result of this test or Image 2 shows what my treeView must show.
image1
image2
Rq: I used image from the article that I talked about it. Also, I used the same approach described in the article (including the class TreeViewItemViewModel.cs )
Base class for all ViewModel
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 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
}
Myxml:
<TreeView ItemsSource="{Binding Regions}" IsEnabled="{Binding EnableTree}" >
<TreeView.ItemContainerStyle>
<!--
This Style binds a TreeViewItem to a TreeViewItemViewModel.
-->
<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>
<ContextMenu x:Key="AddCity" ItemsSource="{Binding AddCityItems}"/>
<HierarchicalDataTemplate
DataType="{x:Type local:StateViewModel}"
ItemsSource="{Binding Children}"
>
<StackPanel Orientation="Horizontal" ContextMenu="{StaticResource AddCity}">
<Image Width="16" Height="16" Margin="3,0" Source="Images\Region.png" />
<TextBlock Text="{Binding RegionName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
RegionViewModel:
`public class StateViewModel : TreeViewItemViewModel
{
readonly State _state;
public ICommand AddCityCommand { get; private set; }
public List<MenuItem> AddCityItems { get; set; }
public StateViewModel(State state, RegionViewModel parentRegion)
: base(parentRegion, true)
{
_state = state;
AddCityItems = new List<MenuItem>();
AddCityCommand = new DelegateCommand<CancelEventArgs>(OnAddCityCommandExecute, OnAddCityCommandCanExecute);
AddCityItems.Add(new MenuItem() { Header = "Add City", Command = AddCityCommand });
}
public string StateName
{
get { return _state.StateName; }
}
protected override void LoadChildren()
{
foreach (City city in Database.GetCities(_state))
base.Children.Add(new CityViewModel(city, this));
}
bool OnAddCityCommandCanExecute(CancelEventArgs parameter)
{
return true;
}
public void OnAddCityCommandExecute(CancelEventArgs parameter)
{
var myNewCity = new city();
Children.Add(new CityViewModel(myNewCity, this));
}
}`
BTW, if I expand my parent node then I click into add City, I have the result as expected but if I don't expand parent node and I click on contextMenu I have another child created in addition to the child I want to create
EDIT
I add the statemnt below to my add() method and I don't have any problem now:
public void OnAddCityCommandExecute(CancelEventArgs parameter)
{
var myNewCity = new city();
Children.Add(new CityViewModel(myNewCity, this));
//the modif
this.Children.Remove(DummyChild);
}
I can see the bug in your code.
Here's the steps to reproduce:
At state node (never expand it first)
Without Expanding the Child upfront, Your StateViewModel's Children contain a DummyChild.
Added 1 new City into the list which cause the HasDummyChild won't work as the count is now 2 in Children's list
Then when you try to expand the node to check the result. Your treelist will have the DummyChild which is a base class that screwed up everything
So, basically that's why "Expand" first is the key of your problem as at that time HasDummyChild still working as it compares .Count == 1. The tree won't remove the DummyChild out from your Children list if you add an extra child to the list that makes .Count == 2.
ADDITIONAL INFO as requested
Just change the HasDummyChild as the following
public bool HasDummyChild
{
//get { return this.Children.Count == 1 && this.Children[0] == DummyChild; }
get { return Children.Any() && Children.Contains(DummyChild); }
}
Related
What I simply want to achieve is to change expand/collapse state of all TreeViewItems from Code Behind. I have created two event handlers for two buttons:
private void Button_Click(object sender, RoutedEventArgs e)
{
for(int i=0;i<trv.Items.Count;i++)
{
TreeViewItem item = (TreeViewItem)(trv.ItemContainerGenerator.ContainerFromIndex(i));
item.IsExpanded = false;
}
}
private void Button_Click1(object sender, RoutedEventArgs e)
{
for (int i = 0; i < trv.Items.Count; i++)
{
TreeViewItem item = (TreeViewItem)(trv.ItemContainerGenerator.ContainerFromIndex(i));
item.IsExpanded = true;
}
}
And my TreeView part of XAML:
<TreeView Name="trv" ItemsSource="{Binding modelItems}">
<TreeView.ItemTemplate>
<DataTemplate>
<TreeViewItem ItemsSource="{Binding modelSubItems}">
<TreeViewItem.Header>
<Grid Width="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding itemId}"/>
<TextBox Text="{Binding itemName}"/>
</TreeViewItem.Header>
<TreeViewItem.ItemTemplate>
<DataTemplate>
<Grid Margin="-20,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding subItemId}"/>
<TextBox Text="{Binding subItemName}"/>
</Grid>
</DataTemplate>
<TreeViewItem.ItemTemplate>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
This does not work, TreeView items do not react on IsExpanded changing from Code Behind.
Many sources say that the problem is in DataTemplate. So, I have changed my XAML adding TreeView.ItemContainerStyle:
<TreeView Name="trv" ItemsSource="{Binding modelItems}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<DataTemplate>
<TreeViewItem ItemsSource="{Binding modelSubItems}">
<TreeViewItem.Header>
<Grid Width="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding itemId}"/>
<TextBox Text="{Binding itemName}"/>
</TreeViewItem.Header>
<TreeViewItem.ItemTemplate>
<DataTemplate>
<Grid Margin="-20,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding subItemId}"/>
<TextBox Text="{Binding subItemName}"/>
</Grid>
</DataTemplate>
<TreeViewItem.ItemTemplate>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
And now. Where should I place IsExpanded definition? In ModelView, in Model? I have tried both, no luck. When placed in ModelView I am getting in output:
System.Windows.Data Error: 40 : BindingExpression path error: 'IsExpanded' property not found on 'object' ''modelItems' (HashCode=43304686)'. BindingExpression:Path=IsExpanded; DataItem='modelItems' (HashCode=43304686); target element is 'TreeViewItem' (Name=''); target property is 'IsExpanded' (type 'Boolean')
When placed in Model, no Binding errors, but still doesn't work.
Of course in both (Model and ModelView), I have INotifyPropertyChanged implemented, which generally works:
public class ModelItem : INotifyPropertyChanged
{
(...)
public event PropertyChangedEventHandler PropertyChanged;
(...)
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
The way I handle WPF TreeView controls in a MVVM context is through the use of a common base class to hold the data for each node.
/// <summary>
/// A base class for items that can be displayed in a TreeView or other hierarchical display
/// </summary>
public class perTreeViewItemViewModelBase : perViewModelBase
{
// a dummy item used in lazy loading mode, ensuring that each node has at least one child so that the expand button is shown
private static perTreeViewItemViewModelBase LazyLoadingChildIndicator { get; }
= new perTreeViewItemViewModelBase { Caption = "Loading Data ..." };
private bool InLazyLoadingMode { get; set; }
private bool LazyLoadTriggered { get; set; }
private bool LazyLoadCompleted { get; set; }
private bool RequiresLazyLoad => InLazyLoadingMode && !LazyLoadTriggered;
// Has Children been overridden (e.g. to point at some private internal collection)
private bool LazyLoadChildrenOverridden => InLazyLoadingMode && !Equals(LazyLoadChildren, _childrenList);
private readonly perObservableCollection<perTreeViewItemViewModelBase> _childrenList
= new perObservableCollection<perTreeViewItemViewModelBase>();
/// <summary>
/// LazyLoadingChildIndicator ensures a visible expansion toggle button in lazy loading mode
/// </summary>
protected void SetLazyLoadingMode()
{
ClearChildren();
_childrenList.Add(LazyLoadingChildIndicator);
IsExpanded = false;
InLazyLoadingMode = true;
LazyLoadTriggered = false;
LazyLoadCompleted = false;
}
private string _caption;
public string Caption
{
get => _caption;
set => Set(nameof(Caption), ref _caption, value);
}
public void ClearChildren()
{
_childrenList.Clear();
}
/// <summary>
/// Add a new child item to this TreeView item
/// </summary>
/// <param name="child"></param>
public void AddChild(perTreeViewItemViewModelBase child)
{
if (LazyLoadChildrenOverridden)
{
throw new InvalidOperationException("Don't call AddChild for an item with LazyLoad mode set & LazyLoadChildren has been overridden");
}
if (_childrenList.Any() && _childrenList.First() == LazyLoadingChildIndicator)
{
_childrenList.Clear();
}
_childrenList.Add(child);
SetChildPropertiesFromParent(child);
}
protected void SetChildPropertiesFromParent(perTreeViewItemViewModelBase child)
{
child.Parent = this;
// if this node is checked then all new children added are set checked
if (IsChecked.GetValueOrDefault())
{
child.SetIsCheckedIncludingChildren(true);
}
ReCalculateNodeCheckState();
}
protected void ReCalculateNodeCheckState()
{
var item = this;
while (item != null)
{
if (item.Children.Any() && !Equals(item.Children.FirstOrDefault(), LazyLoadingChildIndicator))
{
var hasIndeterminateChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.HasValue);
if (hasIndeterminateChild)
{
item.SetIsCheckedThisItemOnly(null);
}
else
{
var hasSelectedChild = item.Children.Any(c => c.IsEnabled && c.IsChecked.GetValueOrDefault());
var hasUnselectedChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.GetValueOrDefault());
if (hasUnselectedChild && hasSelectedChild)
{
item.SetIsCheckedThisItemOnly(null);
}
else
{
item.SetIsCheckedThisItemOnly(hasSelectedChild);
}
}
}
item = item.Parent;
}
}
private void SetIsCheckedIncludingChildren(bool? value)
{
if (IsEnabled)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
foreach (var child in Children)
{
if (child.IsEnabled)
{
child.SetIsCheckedIncludingChildren(value);
}
}
}
}
private void SetIsCheckedThisItemOnly(bool? value)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
}
/// <summary>
/// Add multiple children to this TreeView item
/// </summary>
/// <param name="children"></param>
public void AddChildren(IEnumerable<perTreeViewItemViewModelBase> children)
{
foreach (var child in children)
{
AddChild(child);
}
}
/// <summary>
/// Remove a child item from this TreeView item
/// </summary>
public void RemoveChild(perTreeViewItemViewModelBase child)
{
_childrenList.Remove(child);
child.Parent = null;
ReCalculateNodeCheckState();
}
public perTreeViewItemViewModelBase Parent { get; private set; }
private bool? _isChecked = false;
public bool? IsChecked
{
get => _isChecked;
set
{
if (Set(nameof(IsChecked), ref _isChecked, value))
{
foreach (var child in Children)
{
if (child.IsEnabled)
{
child.SetIsCheckedIncludingChildren(value);
}
}
Parent?.ReCalculateNodeCheckState();
}
}
}
private bool _isExpanded;
public bool IsExpanded
{
get => _isExpanded;
set
{
if (Set(nameof(IsExpanded), ref _isExpanded, value) && value && RequiresLazyLoad)
{
TriggerLazyLoading();
}
}
}
private bool _isEnabled = true;
public bool IsEnabled
{
get => _isEnabled;
set => Set(nameof(IsEnabled), ref _isEnabled, value);
}
public void TriggerLazyLoading()
{
var unused = DoLazyLoadAsync();
}
private async Task DoLazyLoadAsync()
{
if (LazyLoadTriggered)
{
return;
}
LazyLoadTriggered = true;
var lazyChildrenResult = await LazyLoadFetchChildren()
.EvaluateFunctionAsync()
.ConfigureAwait(false);
LazyLoadCompleted = true;
if (lazyChildrenResult.IsCompletedOk)
{
var lazyChildren = lazyChildrenResult.Data;
foreach (var child in lazyChildren)
{
SetChildPropertiesFromParent(child);
}
// If LazyLoadChildren has been overridden then just refresh the check state (using the new children)
// and update the check state (in case any of the new children is already set as checked)
if (LazyLoadChildrenOverridden)
{
ReCalculateNodeCheckState();
}
else
{
AddChildren(lazyChildren); // otherwise add the new children to the base collection.
}
}
RefreshChildren();
}
/// <summary>
/// Get the children for this node, in Lazy-Loading Mode
/// </summary>
/// <returns></returns>
protected virtual Task<perTreeViewItemViewModelBase[]> LazyLoadFetchChildren()
{
return Task.FromResult(new perTreeViewItemViewModelBase[0]);
}
/// <summary>
/// Update the Children property
/// </summary>
public void RefreshChildren()
{
RaisePropertyChanged(nameof(Children));
}
/// <summary>
/// In LazyLoading Mode, the Children property can be set to something other than
/// the base _childrenList collection - e.g as the union of two internal collections
/// </summary>
public IEnumerable<perTreeViewItemViewModelBase> Children => LazyLoadCompleted
? LazyLoadChildren
: _childrenList;
/// <summary>
/// How are the children held when in lazy loading mode.
/// </summary>
/// <remarks>
/// Override this as required in descendent classes - e.g. if Children is formed from a union
/// of multiple internal child item collections (of different types) which are populated in LazyLoadFetchChildren()
/// </remarks>
protected virtual IEnumerable<perTreeViewItemViewModelBase> LazyLoadChildren => _childrenList;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
// if unselecting we don't care about anything else other than simply updating the property
if (!value)
{
Set(nameof(IsSelected), ref _isSelected, false);
return;
}
// Build a priority queue of operations
//
// All operations relating to tree item expansion are added with priority = DispatcherPriority.ContextIdle, so that they are
// sorted before any operations relating to selection (which have priority = DispatcherPriority.ApplicationIdle).
// This ensures that the visual container for all items are created before any selection operation is carried out.
//
// First expand all ancestors of the selected item - those closest to the root first
//
// Expanding a node will scroll as many of its children as possible into view - see perTreeViewItemHelper, but these scrolling
// operations will be added to the queue after all of the parent expansions.
var ancestorsToExpand = new Stack<perTreeViewItemViewModelBase>();
var parent = Parent;
while (parent != null)
{
if (!parent.IsExpanded)
{
ancestorsToExpand.Push(parent);
}
parent = parent.Parent;
}
while (ancestorsToExpand.Any())
{
var parentToExpand = ancestorsToExpand.Pop();
perDispatcherHelper.AddToQueue(() => parentToExpand.IsExpanded = true, DispatcherPriority.ContextIdle);
}
// Set the item's selected state - use DispatcherPriority.ApplicationIdle so this operation is executed after all
// expansion operations, no matter when they were added to the queue.
//
// Selecting a node will also scroll it into view - see perTreeViewItemHelper
perDispatcherHelper.AddToQueue(() => Set(nameof(IsSelected), ref _isSelected, true), DispatcherPriority.ApplicationIdle);
// note that by rule, a TreeView can only have one selected item, but this is handled automatically by
// the control - we aren't required to manually unselect the previously selected item.
// execute all of the queued operations in descending DispatcherPriority order (expansion before selection)
var unused = perDispatcherHelper.ProcessQueueAsync();
}
}
public override string ToString()
{
return Caption;
}
/// <summary>
/// What's the total number of child nodes beneath this one
/// </summary>
public int ChildCount => Children.Count() + Children.Sum(c => c.ChildCount);
}
The IsExpanded link between data and UI controls that you require is then defined in a global style.
<Style
x:Key="perTreeViewItemContainerStyle"
TargetType="{x:Type TreeViewItem}">
<!-- Link the properties of perTreeViewItemViewModelBase to the corresponding ones on the TreeViewItem -->
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
...
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="ItemContainerStyle" Value="{StaticResource perTreeViewItemContainerStyle}" />
</Style>
You can then build the data for the TreeView as a nested collection of perTreeViewItemViewModelBase descendent items, and set the IsExpanded property of each data item as required. Note that the key tenet of MVVM is separation of UI and data, so not a single mention of TreeViewItem anywhere other than in the style.
More details of this perTreeViewItemViewModelBase class and its usage on my blog post.
I created a view from SampleDockWindowView and I like to go to the other view created window when a button is pressed.
I've already tried Application.Current.Windows. This array is just empty.
window2 win2= new window2();
win2.Show();
I would image something like this but with avalondock tabs. not necessarily new window but just to show the existing window when a button is pressed
The following example shows how to cycle through all document tabs of the custom DocumentManager when a Ctrl + Right key combination is pressed (in this example the DocumentManager control is required to have focus):
MainWindow.xaml
<Window>
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<!--
Border to capture keyboard events of the DocumentManager UserControl.
To capture keys in a different scope i.e. more globally,
move the input bindings to a parent control.
-->
<Border>
<!-- Bind keyboard keys to a ICommand. -->
<Border.InputBindings>
<KeyBinding Key="Right"
Modifiers="Control"
Command="{Binding NavigateToNextDocument}"/>
</Border.InputBindings>
<DocumentManager x:Name="DocumentManager" />
</Border>
</Grid
</Window>
DocumentManager.xaml
<UserControl>
<Grid>
<xceed:DockingManager DocumentsSource="{Binding DocumentMainPool}">
<xceed:DockingManager.LayoutItemTemplate>
<DataTemplate DataType="{x:Type local:Document}">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</xceed:DockingManager.LayoutItemTemplate>
<xceed:DockingManager.LayoutItemContainerStyle>
<Style TargetType="xcad:LayoutItem">
<Setter Property="Title"
Value="{Binding Model.Title}" />
<Setter Property="ToolTip"
Value="{Binding Model.Title}" />
<!-- Important -->
<Setter Property="IsSelected"
Value="{Binding Model.IsSelected, Mode=TwoWay}" />
</Style>
</xceed:DockingManager.LayoutItemContainerStyle>
<xceed:LayoutRoot>
<xceed:LayoutPanel>
<xceed:LayoutDocumentPaneGroup>
<!-- *** Dynamically created content (by view model) *** -->
<xceed:LayoutDocumentPane />
</xceed:LayoutDocumentPaneGroup>
</xceed:LayoutPanel>
</xceed:LayoutRoot>
</xceed:DockingManager>
</Grid>
</UserControl>
MainViewModel.cs
class MainViewModel : INotifyProeprtyChanged
{
public MainViewModel()
{
this.DocumentMainPool = new ObservableCollection<IDocument>()
{
new Document("First Document"),
new Document("Second Document")
};
}
private ObservableCollection<IDocument> documentMainPool;
public ObservableCollection<IDocument> DocumentMainPool
{
get => this.documentMainPool;
set
{
this.documentMainPool = value;
OnPropertyChanged();
}
}
public ICommand NavigateToNextDocument => new RelayCommand(param => CycleNextDocuments());
private void CycleNextDocuments()
{
// Only one or no document -> nothing to cycle through
if (this.DocumentMainPool.Count < 2)
{
return;
}
IDocument currentlySelectedDocument = this.DocumentMainPool.FirstOrDefault(document => document.IsSelected);
int currentDocumentIndex = this.DocumentMainPool.IndexOf(currentlySelectedDocument);
// If last document reached, show first again
if (currentDocumentIndex == this.DocumentMainPool.Count - 1)
{
this.DocumentMainPool.FirstOrDefault().IsSelected = true;
return;
}
IDocument nextDocument = this.DocumentMainPool
.Skip(currentDocumentIndex + 1)
.Take(1)
.FirstOrDefault();
nextDocument.IsSelected = true;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
IDocument.cs
// Important: IsSelected must raise PropertyChanged
public interface IDocument : INotifyPropertyChanged
{
string Title { get; set; }
bool IsSelected { get; set; }
}
Document.cs
public class Document : IDocument
{
public Document(string title)
{
this.Title = title;
}
#region Implementation of IDocument
public string Title { get; set; }
private bool isSelected;
public bool IsSelected
{
get => this.isSelected;
set
{
this.isSelected = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
RelayCommand.cs
Implementation taken from Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern:
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<object> execute) : this(execute, null) { }
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
}
Here is a solution that works with MVVM which makes sense since it is an MVVM framework :-) (have not tried it myself, yet, but the code looks sane).
I've created a DockingManager from AvalonDock content in my project and my request is quite simple: when I add a document to my LayoutDocumentPaneGroup I want it to be Active, Selected, and not only add at the end of the LayoutDocumentPaneGroup still activate on the first document.
I tried to implement an IsActive property to my documentView class but it doesn't work.
My dockingmanager in xaml file is defined as below:
<dock:DockingManager DataContext="{Binding DockManagerViewModel}" DocumentsSource="{Binding Documents}" AnchorablesSource="{Binding Anchorables}">
<dock:DockingManager.Resources>
<!-- add views for specific ViewModels -->
<DataTemplate DataType="{x:Type vmdock:SampleDockWindowViewModel}">
<uscontrol:SampleDockWindowView />
</DataTemplate>
</dock:DockingManager.Resources>
<dock:DockingManager.LayoutItemContainerStyle>
<Style TargetType="{x:Type dockctrl:LayoutItem}">
<Setter Property="Title" Value="{Binding Model.Title}" />
<Setter Property="CloseCommand" Value="{Binding Model.CloseCommand}" />
<Setter Property="CanClose" Value="{Binding Model.CanClose}" />
</Style>
</dock:DockingManager.LayoutItemContainerStyle>
<dock:LayoutRoot>
<dock:LayoutPanel Orientation="Vertical">
<dock:LayoutDocumentPaneGroup>
<dock:LayoutDocumentPane />
</dock:LayoutDocumentPaneGroup>
<dock:LayoutAnchorablePaneGroup>
<dock:LayoutAnchorablePane />
</dock:LayoutAnchorablePaneGroup>
</dock:LayoutPanel>
</dock:LayoutRoot>
</dock:DockingManager>
My documentView is defined with the class as below:
public abstract class DockWindowViewModel : BaseViewModel
{
#region Properties
#region CloseCommand
private ICommand _CloseCommand;
public ICommand CloseCommand
{
get
{
if (_CloseCommand == null)
_CloseCommand = new RelayCommand(call => Close());
return _CloseCommand;
}
}
#endregion
#region IsClosed
private bool _IsClosed;
public bool IsClosed
{
get { return _IsClosed; }
set
{
if (_IsClosed != value)
{
_IsClosed = value;
OnPropertyChanged(nameof(IsClosed));
}
}
}
#endregion
#region CanClose
private bool _CanClose;
public bool CanClose
{
get { return _CanClose; }
set
{
if (_CanClose != value)
{
_CanClose = value;
OnPropertyChanged(nameof(CanClose));
}
}
}
#endregion
#region Title
private string _Title;
public string Title
{
get { return _Title; }
set
{
if (_Title != value)
{
_Title = value;
OnPropertyChanged(nameof(Title));
}
}
}
#endregion
#endregion
public DockWindowViewModel()
{
CanClose = true;
IsClosed = false;
}
public void Close()
{
IsClosed = true;
}
Finally found ! I post the result because I think I'll not be alone...
First, I add a new property to my document view definition:
#region IsSelected
private bool _isSelected = false;
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
}
#endregion
But I had also to implement it as a property in my XAML code, as below:
<dock:DockingManager.LayoutItemContainerStyle>
<Style TargetType="{x:Type dockctrl:LayoutItem}">
<Setter Property="Title" Value="{Binding Model.Title}" />
<Setter Property="CloseCommand" Value="{Binding Model.CloseCommand}" />
<Setter Property="CanClose" Value="{Binding Model.CanClose}" />
**<Setter Property="IsSelected" Value="{Binding Model.IsSelected}" />**
</Style>
</dock:DockingManager.LayoutItemContainerStyle>
Supposed it works the same for all properties of LayoutContent defined there: LayoutDocument defined by AvalonDock
EDIT :
Need to add also "Mode=TwoWay" to programmatically update when the selected content changed, like this:
<Setter Property="IsSelected" Value="{Binding Model.IsSelected, Mode=TwoWay}" />
As I already mentioned in my comment on the accepted answer, this didn't work for me, so I moved on trying. My case may differ, though. I'm using Stylet and my DockManager.DocumentsSource is bound to the Items property of a Conductor<MyViewModelBase>.Collection.OneActive Documents in the ViewModel. In this case I needed to do two things to activate the added document:
Bind DockingManager.ActiveContent to Documents.ActiveItem
After Documents.Items.Add(an_instance_of_my_viewmodel); activate the new ViewModel with Documents.ActivateItem(vm);
I am creating side menu using MasterDetailPage and the list of menu items is implemented using ListView. I want to make custom appearance for selected item:
Background color
Label's text color
ImageSource for icon
How can I do this?
I create field IsActive in my list view item and use DataTrigger binded to this field and set all properties what I need.
In XAML
I set color of selected item to the StackLayout, so I just hide original selected color of ListView (orange in my case on Android)
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal"
Padding="16, 0, 16, 0"
HeightRequest="48">
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout"
Binding="{Binding IsActive}"
Value="True">
<Setter Property="BackgroundColor" Value="{StaticResource menu-background-active}"/>
</DataTrigger>
</StackLayout.Triggers>
<Image Source="{Binding IconSource}"
VerticalOptions="Center"
WidthRequest="20"/>
<Label Text="{Binding Title}" TextColor="Black"
VerticalOptions="Center"
Margin="36,0,0,0">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding IsActive}"
Value="True">
<Setter Property="TextColor" Value="{StaticResource menu-text-active}"/>
</DataTrigger>
</Label.Triggers>
</Label>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
ItemsSource item
Note that this class should implement INotifyPropertyChanged
public class MasterPageItem : INotifyPropertyChanged
{
private string _title;
private string _iconSource;
private bool _isActive;
public string Title
{
get { return _title; }
set
{
_title = value;
OnPropertyChanged(nameof(Title));
}
}
/// <summary>
/// Set or return icon file
/// If IsActive == true
/// will add suffix "_active" to return value,
///
/// Note:
/// Icons file should be the pair"
/// - icon_name.png
/// - icon_name_active.png
///
/// </summary>
public string IconSource
{
get
{
if (!IsActive)
return _iconSource;
return Path.GetFileNameWithoutExtension(_iconSource) + "_active" + Path.GetExtension(_iconSource);
}
set
{
_iconSource = value;
OnPropertyChanged(nameof(IconSource));
}
}
/// <summary>
/// Is menu item is selected
/// </summary>
public bool IsActive
{
get { return _isActive; }
set
{
_isActive = value;
OnPropertyChanged());
}
}
public Type TargetType { get; set; }
// Important for data-binding
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string prop = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
Then in the master page code behind I use ItemSelected event to
change the IsActive property
private void ListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as MasterPageItem;
var items = listView.ItemsSource as IList<MasterPageItem>;
//Select current item and deselect others
for(int i = 0; i<items.Count; i++)
items[i].IsActive = items[i] == item;
if (item != null)
{
ItemSelected?.Invoke(this, item.TargetType);
_activePage = item.TargetType;
}
}
I've been working off of this Q/A and a bunch of others to try and figure this out, but I must be missing something simple:
Bind Items to MenuItem -> use Command
I created this little test application to try and understand context menus and learn about how to wire up the click events to relay commands in the ViewModel, and get access to the currently selected item from the context menu.
Here is the XAML:
<Window x:Class="ContextMenuTest_01.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="105" Width="525"
WindowStartupLocation="CenterScreen"
xmlns:local="clr-namespace:ContextMenuTest_01.ViewModels"
DataContext="MainWindowViewModel">
<Window.Resources>
<ObjectDataProvider x:Key="MainWindowViewModel" ObjectType="{x:Type local:MainWindowViewModel}" IsAsynchronous="True"/>
</Window.Resources>
<!-- CONTEXT MENU -->
<Window.ContextMenu>
<ContextMenu DataContext="MainWindowViewModel" Name="MainWindowContextMenu" PresentationTraceSources.TraceLevel="High">
<MenuItem Header="Skins" ItemsSource="{Binding Source={StaticResource MainWindowViewModel}, Path=Skins}">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Source={StaticResource MainWindowViewModel}, Path=Skins.SkinName}"/>
<Setter Property="Command" Value="{Binding Source={StaticResource MainWindowViewModel}, Path=ContextMenuClickCommand}"/>
<Setter Property="CommandParameter" Value="{Binding Path=SkinName}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
</ContextMenu>
</Window.ContextMenu>
</Window>
ViewModel:
using ContextMenuTest_01.Models;
using System.Collections.ObjectModel;
using ContextMenuTest_01.CommandBase;
using System.Windows;
namespace ContextMenuTest_01.ViewModels
{
/// <summary>Main Window View Model</summary>
class MainWindowViewModel
{
#region Class Variables
/// <summary>The skins</summary>
private ObservableCollection<SkinItem> skins = new ObservableCollection<SkinItem>();
#endregion Class Variables
public RelayCommand<object> ContextMenuClickCommand{ get; private set; }
#region Properties
/// <summary>Gets the skins.</summary>
/// <value>The skins.</value>
public ObservableCollection<SkinItem> Skins
{
get { return this.skins; }
private set { this.skins = value; }
}
#endregion Properties
/// <summary>Initializes a new instance of the <see cref="MainWindowViewModel"/> class.</summary>
public MainWindowViewModel()
{
ContextMenuClickCommand = new RelayCommand<object>((e) => OnMenuItemClick(e));
skins.Add(new SkinItem("Skin Item 1"));
skins.Add(new SkinItem("Skin Item 2"));
skins.Add(new SkinItem("Skin Item 3"));
}
/// <summary>Called when [menu item click].</summary>
public void OnMenuItemClick(object selected)
{
MessageBox.Show("Got to the ViewModel! YAY!!!");
}
}
}
So the function OnMenuItemClick will get hit in the debugger and the message box is shown if I change the following three lines in the ViewModel:
Remove the from the Relay Command definition:
public RelayCommand ContextMenuClickCommand{ get; private set; }
Remove the and (e) from where the RelayCommand is created:
ContextMenuClickCommand = new RelayCommand(() => OnMenuItemClick());
Remove the (object selected) from the OnMenuItemClick public function:
public void OnMenuItemClick()
Then everything works but of course I don't have the currently selected item.
So what am I missing in the XAML that would command parameter from passing the argument SkinName to the RelayCommand?
Also if I leave off the line:
<Setter Property="Header" Value="{Binding Source={StaticResource MainWindowViewModel}, Path=Skins.SkinName}"/>
Then I get the following in my context menu:
Skins -> ContextMenuTest_01.Models.SkinItem
ContextMenuTest_01.Models.SkinItem
ContextMenuTest_01.Models.SkinItem
Which tells me the binding is working correctly, it's just not displaying it correctly, which is why I tried to insert the
<Setter Property="Header"....
but of course that's not working as I would expect it to.
Thanks for your time! Any ideas would be helpful!
I don't have anything in the code behind which is the way it should be when following MVVM.
Here is my skinItem class, not much to speak of, but I figured I would show it before someone asks about it:
using System.Windows.Input;
using System.Windows.Media;
namespace ContextMenuTest_01.Models
{
/// <summary>A small data structure to hold a single skin item.</summary>
public class SkinItem
{
#region Class Variables
/// <summary>The skin name</summary>
private string skinName;
/// <summary>The base skin name</summary>
private string baseSkinName;
/// <summary>The skin path</summary>
private string skinPath;
/// <summary>The action to be taken when switching skins.</summary>
private ICommand action;
/// <summary>The icon of the skin.</summary>
private Brush icon;
#endregion Class Variables
#region Constructors
/// <summary>Initializes a new instance of the <see cref="SkinItem"/> class.</summary>
public SkinItem() { }
/// <summary>Initializes a new instance of the <see cref="SkinItem" /> class.</summary>
/// <param name="newSkinName">The name of the new skin.</param>
/// <param name="baseSkinName">Name of the base skin.</param>
/// <param name="newSkinPath">Optional Parameter: The new skin path.</param>
/// <param name="newSkinAction">Optional Parameter: The new skin action to be taken when switching to the new skin.</param>
/// <param name="newSkinIcon">Optional Parameter: The new skin icon.</param>
public SkinItem(string newSkinName, string baseSkinName = "", string newSkinPath = "", ICommand newSkinAction = null, Brush newSkinIcon = null)
{
if (newSkinName != "")
this.skinName = newSkinName;
if (baseSkinName != "")
this.baseSkinName = baseSkinName;
if (newSkinPath != "")
this.skinPath = newSkinPath;
if (newSkinAction != null)
this.action = newSkinAction;
if (newSkinIcon != null)
this.icon = newSkinIcon;
}
#endregion Constructors
#region Properties
/// <summary>Gets or sets the name of the skin.</summary>
/// <value>The name of the skin.</value>
public string SkinName
{
get { return this.skinName; }
set
{
if (this.skinName != value)
{
this.skinName = value;
//OnPropertyChanged(() => this.SkinName);
}
}
}
/// <summary>Gets or sets the name of the base skin.</summary>
/// <value>The name of the base skin.</value>
public string BaseSkinName
{
get { return this.baseSkinName; }
set
{
if (this.baseSkinName != value)
{
this.baseSkinName = value;
//OnPropertyChanged(() => this.BaseSkinName);
}
}
}
/// <summary>Gets or sets the skin path.</summary>
/// <value>The skin path.</value>
public string SkinPath
{
get { return this.skinPath; }
set
{
if (this.skinPath != value)
{
this.skinPath = value;
//OnPropertyChanged(() => this.SkinPath);
}
}
}
/// <summary>Gets or sets the action.</summary>
/// <value>The action.</value>
public ICommand Action
{
get { return this.action; }
set
{
if (this.action != value)
{
this.action = value;
//OnPropertyChanged(() => this.Action);
}
}
}
/// <summary>Gets or sets the icon.</summary>
/// <value>The icon.</value>
public Brush Icon
{
get { return this.icon; }
set
{
if (this.icon != value)
{
this.icon = value;
//OnPropertyChanged(() => this.Icon);
}
}
}
#endregion Properties
}
}
Oh and I am using Galasoft MVVM-Light generic RelayCommand which is supposed to take parameters, can be found here:
http://mvvmlight.codeplex.com/SourceControl/latest#GalaSoft.MvvmLight/GalaSoft.MvvmLight%20%28NET35%29/Command/RelayCommandGeneric.cs
I'm having a little trouble understanding exactly what you're looking for. But in running your code I see that the names of the Skins are not showing in the context menu. If you remove the source so your setting looks like this for the header:
<!-- CONTEXT MENU -->
<Window.ContextMenu>
<ContextMenu DataContext="MainWindowViewModel" Name="MainWindowContextMenu" PresentationTraceSources.TraceLevel="High">
<MenuItem Header="Skins" ItemsSource="{Binding Source={StaticResource MainWindowViewModel}, Path=Skins}">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Path=SkinName}"/>
<Setter Property="Command" Value="{Binding Source={StaticResource MainWindowViewModel}, Path=ContextMenuClickCommand}"/>
<Setter Property="CommandParameter" Value="{Binding Path=SkinName}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
</ContextMenu>
</Window.ContextMenu>
That will fix your problem. Since you are setting the source on the MenuItem, you change the datacontext for the items within. So you don't need to specify the source again.
Edit:
Also I changed the Path from Skins.SkinName to SkinName
Now I see the text for the items in the menu and when I click on say "Skin Item 1", the value of selected in OnMenuItemClick is "Skin Item 1".