I have the following TreeView:
<TreeView x:Name="Folders" Grid.Column="0" SelectedItemChanged="Folders_SelectedItemChanged" ItemsSource="{Binding Items}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type Local:OpenFolderItem}" ItemsSource="{Binding Children}" >
<StackPanel Orientation="Horizontal" VerticalAlignment="Stretch" Margin="0,2,0,2">
<Image Source="{Binding Path=Image, Mode=OneTime}" Stretch="Fill" />
<TextBlock Text="{Binding Name}" Margin="5,0" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
In the code-behind, I would like to change the root directory so that it auto-expands to that directory.
For example:
The following image shows what happens when I open a dialog:
Say I wish to start with the directory: "E:\Sequences", when I start the dialog, I would like it to look like this:
Here's the VM:
Update 1
public class OpenFolderItem : ViewModelBase
{
#region Private Members
private string m_path;
ICollection<OpenFolderItem> m_children;
bool m_isSelected;
#endregion Private Members
#region Constructors
public OpenFolderItem() { }
public OpenFolderItem(string path) { Path = path; }
#endregion
#region Properties
public bool IsExpanded { get; set; }
public string Name { get; set; }
public string Path
{
get
{
return m_path;
}
set
{
m_path = value;
}
}
public ImageSource Image { get; set; }
public virtual ICollection<OpenFolderItem> Children
{
get { return m_children ?? (m_children = LoadChildren()); }
set { m_children = value; }
}
#endregion Properties
#region Private Functions
private ICollection<OpenFolderItem> LoadChildren()
{
var items = new List<OpenFolderItem>();
try
{
items.AddRange(Directory.GetDirectories(Path).Select(directory => new OpenFolderItem(directory)
{
Name = System.IO.Path.GetFileName(directory),
Image = FileInfoHelper.GetFolderImage(false),
IsExpanded = true
}));
items = items.OrderBy(o => o.Path , new Comparer.NaturalStringComparer()).ToList();
}
catch (UnauthorizedAccessException) { }
catch (ArgumentException) { }
catch (DirectoryNotFoundException) { }
return new ReadOnlyCollection<OpenFolderItem>(items);
}
#endregion Private Functions
}
And here's the Xaml.cs:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
namespace OpenDialogs
{
/// <summary>
/// Interaction logic for OpenDialogView.xaml
/// </summary>
public partial class OpenDialogView
{
#region Private Members
private Window m_window;
#endregion Private Members
#region Constructor
/// <summary>
/// The OpenDialogView's constructor.
/// </summary>
public OpenDialogView()
{
InitializeComponent();
}
#endregion Constructor
#region Properties
public string IconFile { get; set; }
#endregion Properties
#region Private Functions
private void Folders_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
ViewModel.SelectedFolder = e.NewValue as OpenFolderItem;
}
private void OnShow(object sender, OpenDialogEventArgs e)
{
try
{
//var b = new OpenFolderItem();
//b.Name = "Sequences";
//b.Path = #"E:\Sequences";
//ViewModel.SelectedFolder = b;
//ItemCollection ic = Folders.Items;
//string yourNode = "Sequences";
//foreach (TreeViewItem tvi in ic)
//{
// if (yourNode.StartsWith(tvi.Tag.ToString()))
// {
// tvi.IsExpanded = true;
// break;
// }
//}
m_window = new Window
{
Content = this,
SizeToContent = SizeToContent.Manual,
ResizeMode = ResizeMode.CanResizeWithGrip,
WindowStyle = WindowStyle.SingleBorderWindow,
Title = e.Caption,
ShowInTaskbar = false,
Topmost = true,
Height = 600,
Width = 1000,
Owner = e.Owner,
WindowStartupLocation = e.StartupLocation,
};
if (!String.IsNullOrEmpty(IconFile))
m_window.Icon = BitmapFrame.Create(new Uri("pack://application:,,,/" + IconFile, UriKind.RelativeOrAbsolute));
m_window.ShowDialog();
}
catch (Exception)
{
Console.WriteLine();
}
}
private void OnClose(object sender, OpenDialogEventArgs e)
{
m_window.Close();
}
#endregion Private Functions
}
}
I'm pretty sure I'm being blocked by the 'HierarchicalDataTemplate'. Is that correct? Any way of making it work?
You'd have to loop through the nodes and set to necessary nodes IsExpanded properties to true.
Try this code in mainWindow.xaml.cs:
public MainWindow()
{
InitializeComponent();
ExtendTree();
}
private void ExtendTree()
{
ItemCollection ic = treeView.Items;
string yourNode = "Sequences";
foreach (var tvi in ic)
{
if (yourNode.StartsWith((TreeViewItem)tvi.Tag.ToString()))
{
(TreeViewItem)tvi.IsExpanded = true;
break;
}
}
}
Related
again I need your help.
I'm building a WPF app, where I have a MainViewModel for a page, that contents a treeview. This treeview is bound to a ProjectTreeViewModel, that is created by the MainViewModel.
Now my ProjectTreeViewModel catches a click-event (using a relay command), that tells it, which node was clicked.
I need this information inside my MainViewmodel. How do I transfer it there?
EDIT... a runnable example
some Data to show in the tree:
using WpfApp1.Models;
namespace WpfApp1.Dataprovider
{
class PlcAddressData
{
public static PlcAddress GetPlcRootItems(string projectName)
{
if (string.IsNullOrWhiteSpace(projectName))
projectName = "Projekt-Datenpunkte";
return new PlcAddress
{
Name = projectName,
NodeId = 0,
Children =
{
new PlcAddress
{
Name = "Allgemein",
Comment = "allgemeine Datenpunkte",
NodeId = 1,
ParentNodeId = 0
},
new PlcAddress
{
Name = "Infrastruktur",
Comment = "interne Datenpunkte der Infrastruktur",
ParentNodeId = 0,
NodeId = 2
},
new PlcAddress
{
Name = "lokale IOs",
Comment = "Datenpunkte der SPS-Baugruppe",
ParentNodeId = 0,
NodeId = 3,
Children =
{
new PlcAddress
{
Name = "IO 0",
Comment = "first Channel of Plc-IO-Card",
NodeId = 4,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 1",
Comment = "second Channel of Plc-IO-Card",
NodeId = 5,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 2",
Comment = "third Channel of Plc-IO-Card",
NodeId = 6,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 3",
Comment = "forth Channel of Plc-IO-Card",
NodeId = 7,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 4",
Comment = "fifth Channel of Plc-IO-Card",
NodeId = 8,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 5",
Comment = "sixth Channel of Plc-IO-Card",
NodeId = 9,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 6",
Comment = "seventh Channel of Plc-IO-Card",
NodeId = 10,
ParentNodeId = 3
},
new PlcAddress
{
Name = "IO 7",
Comment = "eighth Channel of Plc-IO-Card",
NodeId = 11,
ParentNodeId = 3
}
}
}
}
};
}
}
}
The PlcAddress-model (data for treeview-item):
using System.Collections.Generic;
namespace WpfApp1.Models
{
public class PlcAddress
{
private List<PlcAddress> _children = new List<PlcAddress>();
public List<PlcAddress> Children
{
get { return _children; }
set { _children = value; }
}
public int NodeId { get; set; }
public int ParentNodeId { get; set; }
public string Name { get; set; }
public string Comment { get; set; }
}
}
A RelayCommand:
using System;
using System.Windows.Input;
namespace WpfApp1.ViewModels.Commands
{
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion
#region Constructors
public RelayCommand(Action<object> execute) : this(execute, null){ }
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
_execute = execute ?? throw new ArgumentNullException("execute");
_canExecute = canExecute;
}
#endregion
#region ICommand Members
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
}
}
The MainViewModel:
using WpfApp1.Models;
namespace WpfApp1.ViewModels
{
public class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
{
LoadProjectTree();
}
private void LoadProjectTree()
{
PlcAddress RootItem = Dataprovider.PlcAddressData.GetPlcRootItems("Parent Node of Project");
_projectTree = new ProjectTreeviewModel(RootItem);
_projectTree.PropertyChanged += ProjectTreePropertyChanged;
}
private void ProjectTreePropertyChanged(object sender, PropertyChangedEventArgs e)
{
ProjectTreeviewModel selectedNode = (ProjectTreeviewModel)sender;
System.Console.WriteLine("selectedNode changed:" + selectedNode.SelectedNode);
SelectedNode = selectedNode.SelectedNode;
//MessageBox.Show("Some Property changed");
}
#region Properties
private string _selectedNode;
public string SelectedNode {
get { return _selectedNode; }
set
{
_selectedNode = value;
OnPropertyChanged("SelectedNode");
}
}
private ProjectTreeviewModel _projectTree;
public ProjectTreeviewModel ProjectTree
{
get { return _projectTree; }
}
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
The PlcAddressViewModel to show as item in the tree
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using WpfApp1.Models;
namespace WpfApp1.ViewModels
{
public class PlcAddressViewModel : INotifyPropertyChanged
{
#region Data
private Collection<PlcAddressViewModel> _children;
readonly PlcAddressViewModel _parent;
readonly PlcAddress _plcAddress;
bool _isExpanded;
bool _isSelected;
#endregion Data
#region Constructors
public PlcAddressViewModel(PlcAddress plcAddress) : this(plcAddress, null)
{
}
private PlcAddressViewModel(PlcAddress plcAddress, PlcAddressViewModel parent)
{
_parent = parent;
_plcAddress = plcAddress;
_children = new Collection<PlcAddressViewModel>(
(from child in _plcAddress.Children
select new PlcAddressViewModel(child, this))
.ToList<PlcAddressViewModel>());
}
#endregion Constructors
#region AddressProperties
public Collection<PlcAddressViewModel> Children
{
get { return _children; }
set { _children = value; }
}
public string Name
{
get { return _plcAddress.Name; }
}
public string Comment
{
get { return _plcAddress.Comment; }
}
#endregion AddressProperties
#region Presentation Members
#region IsExpanded
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;
}
}
#endregion IsExpanded
#region IsSelected
public bool IsSelected
{
get
{
if (_isSelected)
{
//Console.WriteLine("Nodeselected: " + this._plcAddress.Name);
}
return this._isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
}
}
#endregion IsSelected
#region Parent
public PlcAddressViewModel 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
}
}
The ProjectTreeViewModel where the selectionchanged is recognized
using System;
using System.Collections.ObjectModel;
using WpfApp1.Models;
using WpfApp1.ViewModels.Commands;
namespace WpfApp1.ViewModels
{
public class ProjectTreeviewModel : INotifyPropertyChanged
{
#region Data
public RelayCommand TreeNodeSelected { get; private set; }
readonly ReadOnlyCollection<PlcAddressViewModel> _rootNodes;
readonly PlcAddressViewModel _rootAddress;
#endregion Data
#region Constructor
public ProjectTreeviewModel(PlcAddress rootAddress)
{
_rootAddress = new PlcAddressViewModel(rootAddress);
_rootNodes = new ReadOnlyCollection<PlcAddressViewModel>(
new PlcAddressViewModel[]
{
_rootAddress
});
TreeNodeSelected = new RelayCommand(ExecuteTreeNodeSelected, canExecuteMethod);
}
#endregion Constructor
#region Properties
private string _selectedNode;
public string SelectedNode
{
get { return _selectedNode; }
set
{
_selectedNode = value;
OnPropertyChanged("SelectedNode");
}
}
#endregion
#region RootNode
public ReadOnlyCollection<PlcAddressViewModel> ProjectNode
{
get { return _rootNodes; }
}
#endregion RootNode
#region Commands
private bool canExecuteMethod(object parameter)
{
return true;
}
private void ExecuteTreeNodeSelected(object parameter)
{
PlcAddressViewModel selectedNode = (PlcAddressViewModel)parameter;
Console.WriteLine("Found this node: " + selectedNode.Name);
SelectedNode = selectedNode.Name;
}
#endregion Commands
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
last but not least, the MainWindow.xaml
<Window x:Class="WpfApp1.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:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
xmlns:viewmodels="clr-namespace:WpfApp1.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
>
<DockPanel LastChildFill="True">
<StackPanel Margin="5" Orientation="Horizontal">
<TreeView DataContext="{Binding ProjectTree}" ItemsSource="{Binding ProjectNode}" DockPanel.Dock="Left"
x:Name="ProjectTree" Margin="0 0 2 0" Grid.Column="0">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction Command="{Binding TreeNodeSelected}"
CommandParameter="{Binding ElementName=ProjectTree, Path=SelectedItem}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<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">
<TextBlock VerticalAlignment="Center" Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
<!-- following Texblock is bound to a MainViewModels property -->
<TextBlock Text="{Binding SelectedNode}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DockPanel>
</Window>
... and its codebehind:
using System.Windows;
using WpfApp1.ViewModels;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
}
...after some hours of experiments, I solved it. The solution is in the snippets above and I don't know if that is the best way. I did it the folling way:
The Command-method ExecuteTreeNodeSelected changes the public property SelectedNode. That fires a notification OnPropertyChanged("SelectedNode");.
When creating the Treeviewmodel, in MainViewModel, I added an eventlistener to the PropertyChanged-event of the TreeViewModel _projectTree.PropertyChanged += ProjectTreePropertyChanged;. This Event changes the SelectedNode-property of the MainViewModel, that notifies the UI.
Thanks for your patience
This is the ViewModel base class that I use for hierarchical data to be displayed in a TreeView control.
/// <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 ot 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);
}
Amongst other features, it includes a Parent property, so each item in the data structure knows its immediate ancestor.
Rather than using an event to handle an item being clicked, I've created a behaviour to add a bindable selected item property to a TreeView. This allows a change of selection to be detected for means other than a direct mouse click, such as the user pressing the arrow keys.
public class perTreeViewHelper : Behavior<TreeView>
{
public object BoundSelectedItem
{
get => GetValue(BoundSelectedItemProperty);
set => SetValue(BoundSelectedItemProperty, value);
}
public static readonly DependencyProperty BoundSelectedItemProperty =
DependencyProperty.Register("BoundSelectedItem",
typeof(object),
typeof(perTreeViewHelper),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnBoundSelectedItemChanged));
private static void OnBoundSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue is perTreeViewItemViewModelBase item)
item.IsSelected = true;
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
base.OnDetaching();
}
private void OnTreeViewSelectedItemChanged(object obj, RoutedPropertyChangedEventArgs<object> args)
{
BoundSelectedItem = args.NewValue;
}
}
More details on my blog post
I have an object class that derives from BoxView. The object has TouchEffect attached and when the OnTouchAction starts, I change the object's properties.
I would also like to change the property of a label based on the text attached to the string that label is bound to.
What I did is I created an instance of the page that contains bindable string and label, then, I tried to change the value of string by referencing it in code inside the OnTouchAction method.
I don't get errors and the Breakpoint tells me that code arrives to the line, but the label is not being updated.
I am trying to update the string from the class that is not the same where the string is.
Is there anyone who could help me out here?
class Element : BoxView
{
List<long> ids = new List<long>();
MainPage mainPage = new MainPage();
public event EventHandler StatusChanged;
public Element()
{
TouchEffect effect = new TouchEffect();
effect.TouchAction += OnTouchEffectAction;
Effects.Add(effect);
}
public Color DefaultColor { set; get; }
public Color HighlightColor { set; get; }
public bool IsPressed { private set; get; }
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Pressed:
AddToList(args.Id);
mainPage.LeftLabelText = "entered";
break;
case TouchActionType.Entered:
if (args.IsInContact)
{
AddToList(args.Id);
mainPage.LeftLabelText = "entered";
}
break;
case TouchActionType.Moved:
break;
case TouchActionType.Released:
case TouchActionType.Exited:
RemoveFromList(args.Id);
break;
}
}
void AddToList(long id)
{
if (!ids.Contains(id))
{
ids.Add(id);
}
CheckList();
}
void RemoveFromList(long id)
{
if (ids.Contains(id))
{
ids.Remove(id);
}
CheckList();
}
void CheckList()
{
if (IsPressed != ids.Count > 0)
{
IsPressed = ids.Count > 0;
Color = IsPressed ? HighlightColor : DefaultColor;
mainPage.LeftLabelText = "entered";
StatusChanged?.Invoke(this, EventArgs.Empty);
}
}
}
and the MainPage relevant code:
Element element = new Element();
element.HighlightColor = Color.Accent;
element.DefaultColor = Color.Transparent;
element.Color = bar.DefaultColor;
element.HeightRequest = sGrid.Height;
element.VerticalOptions = LayoutOptions.End;
(...) // adding element view to the grid.
The string:
public string _leftLabelText = "testing";
public string LeftLabelText
{
get => _leftLabelText;
set
{
_leftLabelText = value;
NotifyPropertyChanged("LeftLabelText");
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged2;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged2?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
<Label x:Name="leftLabel" x:FieldModifier="public" Text="{Binding LeftLabelText, Mode=TwoWay}" TextColor="Black" FontSize="10" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand"/>
Is it possible to have one ViewModel for multiple dynamic Tabs? Meaning that, whenever I create a new tab, it should use the same instance of ViewModel so I can retrieve information and also prevent each Tab from sharing data/showing the same data.
The setting I'm thinking of using it in would be for a payroll application where each employee's payslip can be updated from each tab. So the information should be different in each Tab.
Is this possible?
Update: Added code
MainViewModel where Tabs Collection is handled:
public ObservableCollection<WorkspaceViewModel> Workspaces { get; set; }
public MainViewModel()
{
Workspaces = new ObservableCollection<WorkspaceViewModel>();
Workspaces.CollectionChanged += Workspaces_CollectionChanged;
}
void Workspaces_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count != 0)
foreach (WorkspaceViewModel workspace in e.NewItems)
workspace.RequestClose += this.OnWorkspaceRequestClose;
if (e.OldItems != null && e.OldItems.Count != 0)
foreach (WorkspaceViewModel workspace in e.OldItems)
workspace.RequestClose -= this.OnWorkspaceRequestClose;
}
private void OnWorkspaceRequestClose(object sender, EventArgs e)
{
CloseWorkspace();
}
private DelegateCommand _exitCommand;
public ICommand ExitCommand
{
get { return _exitCommand ?? (_exitCommand = new DelegateCommand(() => Application.Current.Shutdown())); }
}
private DelegateCommand _newWorkspaceCommand;
public ICommand NewWorkspaceCommand
{
get { return _newWorkspaceCommand ?? (_newWorkspaceCommand = new DelegateCommand(NewWorkspace)); }
}
private void NewWorkspace()
{
var workspace = new WorkspaceViewModel();
Workspaces.Add(workspace);
SelectedIndex = Workspaces.IndexOf(workspace);
}
private DelegateCommand _closeWorkspaceCommand;
public ICommand CloseWorkspaceCommand
{
get { return _closeWorkspaceCommand ?? (_closeWorkspaceCommand = new DelegateCommand(CloseWorkspace, () => Workspaces.Count > 0)); }
}
private void CloseWorkspace()
{
Workspaces.RemoveAt(SelectedIndex);
SelectedIndex = 0;
}
private int _selectedIndex = 0;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
_selectedIndex = value;
OnPropertyChanged("SelectedIndex");
}
}
WorkspaceViewModel:
public PayslipModel Payslip { get; set; }
public WorkspaceViewModel()
{
Payslip = new PayslipModel();
SaveToDatabase = new DelegateCommand(Save, () => CanSave);
SelectAll = new DelegateCommand(Select, () => CanSelect);
UnSelectAll = new DelegateCommand(UnSelect, () => CanUnSelect);
}
public ICommand SaveToDatabase
{
get; set;
}
private bool CanSave
{
get { return true; }
}
private async void Save()
{
try
{
MessageBox.Show(Payslip.Amount.ToString());
}
catch (DbEntityValidationException ex)
{
foreach (var en in ex.EntityValidationErrors)
{
var exceptionDialog = new MessageDialog
{
Message = { Text = string.Format("{0}, {1}", en.Entry.Entity.GetType().Name, en.Entry.State) }
};
await DialogHost.Show(exceptionDialog, "RootDialog");
foreach (var ve in en.ValidationErrors)
{
exceptionDialog = new MessageDialog
{
Message = { Text = string.Format("{0}, {1}", ve.PropertyName, ve.ErrorMessage) }
};
await DialogHost.Show(exceptionDialog, "RootDialog");
}
}
}
catch (Exception ex)
{
var exceptionDialog = new MessageDialog
{
Message = { Text = string.Format("{0}", ex) }
};
await DialogHost.Show(exceptionDialog, "RootDialog");
}
}
public event EventHandler RequestClose;
private void OnRequestClose()
{
if (RequestClose != null)
RequestClose(this, EventArgs.Empty);
}
private string _header;
public string Header
{
get { return _header; }
set
{
_header = value;
OnPropertyChanged("Header");
}
}
Payroll UserControl where WorkspaceViewModel is DataContext:
public Payroll()
{
InitializeComponent();
DataContext = new WorkspaceViewModel();
}
Payroll.xaml Tabcontrol:
<dragablz:TabablzControl ItemsSource="{Binding Workspaces}" SelectedIndex="{Binding SelectedIndex}" BorderBrush="{x:Null}">
<dragablz:TabablzControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</dragablz:TabablzControl.ItemTemplate>
<dragablz:TabablzControl.ContentTemplate>
<DataTemplate>
<ContentControl Margin="16">
<local:TabLayout DataContext="{Binding Path=Payslip, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" x:Name="tabLayout"/>
</ContentControl>
</DataTemplate>
</dragablz:TabablzControl.ContentTemplate>
</dragablz:TabablzControl>
This works as expected, each tab displays different info and bindings work okay. However, I'm unable to retrieve the info in the MessageBox.
I'm not sure if I totally understand your question but if you need a Window with a tabcontrol, in which each tab refers to an employee, then you will have to bind the ItemsSource of the tabcontrol to a list of the ViewModel.
It is not possible to bind all tabpages to the same instance because then the tabpages will all do the same, and show the same information.
I couldn't get it to work the way I had it, so I placed the save button inside the view that has DataContext set to where employee's info are loaded and got it to work from there, since it directly accesses the properties.
ViewModels should have a 1:1 relationship with the model. In your TabControl's DataContext, let's say you have properties like:
public ObservableCollection<EmployeeViewModel> Employees {get;set;}
public EmployeeViewModel CurrentEmployee
{
get { return _currentEmployee;}
set
{
_currentEmployee = value;
OnPropertyChanged("CurrentEmployee");
}
}
where Employees is bound to ItemsSource of the TabControl, and CurrentEmployee to CurrentItem. To create a new tab:
var employee = new Employee();
var vm = new EmployeeViewModel(employee);
Employees.Add(vm);
CurrentEmployee = vm;
If you want a save button outside of the TabControl, just set its DataContext to CurrentEmployee.
I hope this helps!
Edit:
Two things I think are causing problems:
Payroll.xaml should be bound to MainViewModel since that's where the Workspaces collection is.
Do not instantiate ViewModels in your view's code behind. Use a DataTemplate instead (see this question).
Take a look at Josh Smith's MVVM demo app (source code)
I'm trying to find a simple approach for data binding in WPF.
I'm using the INotifyPropertyChanged interface and it works fine if it's implemented on an abstract base class and inherited by objects that have bound members.
public partial class MainWindow : Window
{
public static MainWindow Instance;
private readonly Vm _vm;
public MainWindow ()
{
InitializeComponent();
DataContext = _vm = new Vm
{
Button1 = new Vm.ObservableButton(button1, new List<string> { "Paused", "Logging" }, false),
Button2 = new Vm.ObservableToggleButton(button2, new List<string> { "Log All", "Log VBA" }, false),
};
}
private class Vm
{
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged ([CallerMemberName] string propName = "")
{
var pc = PropertyChanged;
if (pc != null)
pc(this, new PropertyChangedEventArgs(propName));
}
}
public class ObservableButton : ObservableObject
{
private readonly Button _b;
private readonly List<string> _options;
private string _content;
public string Content
{
get { return _content; }
set
{
if (_content == value) return;
_content = value;
OnPropertyChanged();
}
}
public Boolean On { set; private get; }
public ObservableButton (Button b, List<string> options, Boolean on = true)
{
_b = b;
_options = options;
_b.Click += Click;
On = on;
Content = On ? _options[0] : _options[1];
}
public void Click (object sender, RoutedEventArgs e)
{
On = !On;
Content = On ? _options[0] : _options[1];
}
}
public class ObservableToggleButton : ObservableObject
{
private readonly ToggleButton _b;
private readonly List<string> _options;
private string _content;
public string Content
{
get { return _content; }
private set
{
if (_content == value) return;
_content = value;
OnPropertyChanged();
}
}
private Boolean _on;
public Boolean On
{
private get { return _on; }
set
{
if (_on == value) return;
_on = value;
Content = value ? _options[0] : _options[1];
}
}
public ObservableToggleButton (ToggleButton b, List<string> options, Boolean on = true)
{
_b = b;
_options = options;
On = on;
Content = _b.IsChecked ?? false ? _options[0] : _options[1];
}
public void Push ()
{
var peer = new ToggleButtonAutomationPeer(_b);
var toggleProvider = peer.GetPattern(PatternInterface.Toggle) as IToggleProvider;
if (toggleProvider != null) toggleProvider.Toggle();
//On = !On;
}
}
public ObservableButton Button1 { get; set; }
public ObservableToggleButton Button2 { get; set; }
public Vm ()
{
}
}
}
<Grid Margin="0,0,183,134">
<Button x:Name="button1" Content="{Binding Button1.Content}" HorizontalAlignment="Left" Margin="112,134,0,0" VerticalAlignment="Top" Width="75"/>
<ToggleButton x:Name="button2" IsChecked="{Binding Button2.On, Mode=OneWayToSource}" Content="{Binding Button2.Content}" HorizontalAlignment="Left" Margin="206,134,0,0" VerticalAlignment="Top"/>
</Grid>
I wanted to try doing this without burning the base class though, so I implemented INotifyPropertyChanged on the View Model and routed the change events from the bound members, back through the single interface on the View Model. Even though the Binding Object has a reference to the Source and the correct property name, this fails silently.
I figured that it doesn't work because the Binding Object does some type checking, so I made a fake implementation on the bound properties and it works. Here is the code for that scenario...
public partial class MainWindow : Window
{
public static MainWindow Instance;
public MainWindow ()
{
InitializeComponent();
DataContext = new ViewModel
{
Button1 = new ViewModel.ObservableButton(button1, new List<string> { "Paused", "Logging" }, false),
Button2 = new ViewModel.ObservableToggleButton(button2, new List<string> { "Log All", "Log VBA" }, false),
};
}
public class ViewModel : INotifyPropertyChanged
{
private static ViewModel _instance;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged<T> (T control, [CallerMemberName] string propName = "")
{
var pc = PropertyChanged;
if (pc != null)
pc(control, new PropertyChangedEventArgs(propName));
}
public class ObservableButton : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged () {}
private readonly Button _b;
private readonly List<string> _options;
private string _content;
public string Content
{
get { return _content; }
set
{
if (_content == value) return;
_content = value;
_instance.OnPropertyChanged(this);
}
}
public Boolean On { set; private get; }
public ObservableButton (Button b, List<string> options, Boolean on = true)
{
_b = b;
_options = options;
_b.Click += Click;
On = on;
Content = On ? _options[0] : _options[1];
}
public void Click (object sender, RoutedEventArgs e)
{
On = !On;
Content = On ? _options[0] : _options[1];
}
}
public class ObservableToggleButton : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged () {}
private readonly ToggleButton _b;
private readonly List<string> _options;
private string _content;
public string Content
{
get { return _content; }
private set
{
if (_content == value) return;
_content = value;
_instance.OnPropertyChanged(this);
}
}
private Boolean _on;
public Boolean On
{
private get { return _on; }
set
{
if (_on == value) return;
_on = value;
Content = value ? _options[0] : _options[1];
}
}
public ObservableToggleButton (ToggleButton b, List<string> options, Boolean on = true)
{
_b = b;
_options = options;
On = on;
Content = _b.IsChecked ?? false ? _options[0] : _options[1];
}
}
public ObservableButton Button1 { get; set; }
public ObservableToggleButton Button2 { get; set; }
public ViewModel ()
{
_instance = this;
}
}
}
<Grid Margin="0,0,183,134">
<Button x:Name="button1" Content="{Binding Button1.Content}" HorizontalAlignment="Left" Margin="112,134,0,0" VerticalAlignment="Top" Width="75"/>
<ToggleButton x:Name="button2" IsChecked="{Binding Button2.On, Mode=OneWayToSource}" Content="{Binding Button2.Content}" HorizontalAlignment="Left" Margin="206,134,0,0" VerticalAlignment="Top"/>
</Grid>
So you can see that, even though the interface on the ObservableButton and ObservableToggleButton types are still routing the change notification through their parent, the Binding Object is happy because they toe the line on type.
Is there a good reason why the the child object needs to implement the interface even though there is already everything need to complete the binding without it?
I try to provide a clear example how this should be done in WPF instead of trying to fix the OP question.
XAML
<StackPanel>
<StackPanel.Resources>
<BooleanToVisibilityConverter x:Key="bToV" />
</StackPanel.Resources>
<!--bind the text to the viewmodel content. Use a bool to visibilty converter to convert from true to Visible-->
<TextBlock
Text="{Binding Path=Content}"
Visibility="{Binding Path=IsContentVisible, Converter={StaticResource bToV}}" />
<!--Use a two way binding to sync the IsChecked property with the viewmodel-->
<ToggleButton IsChecked="{Binding Path=IsContentVisible,Mode=TwoWay}"
Content="{Binding Path=ToogleActionName}" />
</StackPanel>
code behind
to keep your project structure clear I warmly suggest to put each class in a separate file. However I put all 3 classes into one single file for easier posting.
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
namespace WpfApplication4
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ContentViewModel() { Content = "foo" };
}
}
public class ContentViewModel : ViewModelBase
{
private string _toogleActionName = "turn it off";
private bool _isContentVisible = true;
private string _content;
public bool IsContentVisible
{
get
{
return _isContentVisible;
}
set
{
_isContentVisible = value;
//switch action name
if (value)
ToogleActionName = "turn it off";
else
ToogleActionName = "turn it on";
OnPropertyChanged();
}
}
public string Content
{
get
{
return _content;
}
set
{
_content = value;
OnPropertyChanged();
}
}
public string ToogleActionName
{
get
{
return _toogleActionName;
}
set
{
_toogleActionName = value;
OnPropertyChanged();
}
}
}
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
I hope this is showing how WPF is supposed to work with the MVVM pattern.
The problem there is that the ViewModel on the first example:
private class Vm
{
...
}
Does not implement INofityPropertyChanged interface, therefore whenever you say that you DataContext is "Vm", the binding would not know that a property has changed because the view model it is not implementing INotifyPropertyChanged...
And on the second example, it is working because you are implementing a INofityPropertyChanged on the view model class
public class ViewModel : INotifyPropertyChanged
{
...
}
Note that it doesn't matter if your child classes implements INotifyPropertyChanged if your base class doesn't implement it too and your base class is observing changes on the children and raises the changes as "its own"...
I'm doing an app that uses the Microsoft.Maps and I'm having to problems that I hope that you can help me solving. (Taking into account that I'm using C# with MVVM architecture and XAML)
Q1: I'm defining the Center position as (0,0) and I'm trying to change it programatically but for some reason before the map is "Loaded" the center doesn't change. How can I do it?
Q2: I'm adding my pushpinn and some friends pushpins (their location are given) and I want to focus the map as somekind of boundedbox to focus only myself and my friends. How can I do this keeping the architecture?
XAML VIEW
<Grid>
<m:Map x:Name="MyMap" CredentialsProvider="mycredential" Center="{Binding MyCurrentPos}" ZoomLevel="16">
<m:MapItemsControl ItemsSource="{Binding PushpinList}">
<m:MapItemsControl.ItemTemplate>
<DataTemplate>
<m:Pushpin Location="{Binding location}" Background="Red"/>
</DataTemplate>
</m:MapItemsControl.ItemTemplate>
</m:MapItemsControl>
</m:Map>
</Grid>
C# VIEWMODEL
public OverViewViewModel() {
MyCurrentPos = "0,0";
getLocation();
}
private ObservableCollection<PushPinHelper> _pushpinList;
public ObservableCollection<PushPinHelper> PushpinList
{
get
{
if (_pushpinList == null)
{
_pushpinList = new ObservableCollection<PushPinHelper>();
}
return _pushpinList;
}
set { _pushpinList = value; }
}
public void getLocation()
{
GeoCoordinateWatcher watcher = new GeoCoordinateWatcher();
watcher.PositionChanged += Watcher_PositionChanged;
watcher.TryStart(false, TimeSpan.FromMilliseconds(1000)); // try start
}
private void Watcher_PositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> e)
{
GeoCoordinate geo = new GeoCoordinate(e.Position.Location.Latitude, e.Position.Location.Longitude);
PushPinHelper pp = new PushPinHelper(geo.ToString());
MyCurrentPos = geo.ToString();
OnPropertyChanged("MyCurrentPos");
PpHelper = pp;
OnPropertyChanged("PpHelper");
addMyself(pp);
}
public void addMyself(PushPinHelper geo)
{
PushpinList.Add(geo);
OnPropertyChanged("PushpinList");
}
private PushPinHelper _ppHelper;
public PushPinHelper PpHelper
{
get { return _ppHelper; }
set
{
_ppHelper = value;
}
}
// CLASS PushpinHelper
public class PushPinHelper
{
public PushPinHelper(string loc)
{
location = loc;
}
public string location { get; set; }
}