How to change a property of parent-viewmodel - c#

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

Related

WPF Notify content change when it's property changed

I've set label Content to some custom class:
<Label>
<local:SomeContent x:Name="SomeContent" some="abc" />
</Label>
This properly displays "abc" in a view. However I can't figure out how do I notify the Label that the content property have changed i.e. this:
SomeContent.some = "xyz";
Will not cause the label to update it's view.
I know I can set binding to label's Content property. I have already like 7 different, working methods to achieve automatic update. However I'm interested in this particular behavior because it will save me a ton of work in some scenarios i.e the requirements are:
Label content is always the same SomeContent instance, only it's properties are changed.
No label content binding. The label should take a content object and refresh whenever the content is modified.
Initial value of some property can be set in XAML
some property can be changed in code, causing label refresh.
Am I missing something, or it's not possible?
This is my current implementation of SomeContent:
public class SomeContent : DependencyObject, INotifyPropertyChanged {
public static readonly DependencyProperty someProperty =
DependencyProperty.Register(nameof(some), typeof(string),
typeof(SomeContent),
new PropertyMetadata("", onDPChange)
);
private static void onDPChange(DependencyObject d, DependencyPropertyChangedEventArgs e) {
//throw new NotImplementedException();
(d as SomeContent).some = e.NewValue as String;
}
public event PropertyChangedEventHandler PropertyChanged;
public string some {
get => (string)GetValue(someProperty);
set {
SetValue(someProperty, value);
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(some))
);
}
}
public override string ToString() => some;
}
I turns out it's not possible to do it without third side code. So I wrote a helper class to do it easy now.
Dynamic object
public class SomeContent : IChangeNotifer {
public event Action<object> MODIFIED;
private string _some;
public string some {
get => _some;
set {
_some = value;
MODIFIED?.Invoke(this);
}
}
public override string ToString() => some;
}
You can add it to xaml file and it will be updated automatically. Single additional step is to add UIReseter somewhere bellow the elements that suppose to be auto-updated but that is needed only one for multiple contents in a tree.
Usage
<Window x:Class="DependencyContentTest.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"
xmlns:local="clr-namespace:DependencyContentTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel>
<local:UIReseter />
<Label>
<local:SomeContent x:Name="SomeContent" some="abcd" />
</Label>
<Grid>
<Label>
<local:SomeContent x:Name="nested" some="nyest"/>
</Label>
</Grid>
</StackPanel>
</Window>
MainWindow code
public partial class MainWindow : Window {
private Timer t;
public MainWindow() {
InitializeComponent();
t = new Timer(onTimer, null, 5000, Timeout.Infinite);
MouseDown += (s,e) => { SomeContent.some = "iii"; };
}
private void onTimer(object state) {
Dispatcher.Invoke(() => {
SomeContent.some = "aaaa";
nested.some = "xxx";
});
}
}
And this is the helper class that handles the update
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using H = System.Windows.LogicalTreeHelper;
using FE = System.Windows.FrameworkElement;
using DO = System.Windows.DependencyObject;
using System.Reflection;
using System.Windows.Markup;
namespace DependencyContentTest
{
public interface IChangeNotifer {
/// <summary>Dispatched when this object was modified.</summary>
event Action<object> MODIFIED;
}
/// <summary>This element tracks nested <see cref="IChangeNotifer"/> descendant objects (in logical tree) of this object's parent element and resets a child in it's panel property.
/// Only static (XAML) objects are supported i.e. object added to the tree dynamically at runtime will not be tracked.</summary>
public class UIReseter : UIElement {
public int searchDepth { get; set; } = int.MaxValue;
protected override void OnVisualParentChanged(DO oldParent){
if (VisualParent is FE p) p.Loaded += (s, e) => bind(p);
}
private void bind(FE parent, int dl = 0) {
if (parent == null || dl > searchDepth) return;
var chs = H.GetChildren(parent);
foreach (object ch in chs) {
if (ch is UIReseter r && r != this) throw new Exception($#"There's overlapping ""{nameof(UIReseter)}"" instance in the tree. Use single global instance of check ""{nameof(UIReseter.searchDepth)}"" levels.");
if (ch is IChangeNotifer sc) trackObject(sc, parent);
else bind(ch as FE, ++dl);
}
}
private Dictionary<IChangeNotifer, Reseter> tracked = new Dictionary<IChangeNotifer, Reseter>();
private void trackObject(IChangeNotifer sc, FE parent) {
var cp = getContentProperty(parent);
if (cp == null) return;
var r = tracked.nev(sc, () => new Reseter {
child = sc,
parent = parent,
content = cp,
});
r.track();
}
private PropertyInfo getContentProperty(FE parent) {
var pt = parent.GetType();
var cp = parent.GetType().GetProperties(
BindingFlags.Public |
BindingFlags.Instance
).FirstOrDefault(i => Attribute.IsDefined(i,
typeof(ContentPropertyAttribute)));
return cp ?? pt.GetProperty("Content");
}
private class Reseter {
public DO parent;
public IChangeNotifer child;
public PropertyInfo content;
private bool isTracking = false;
/// <summary>Function called by <see cref="IChangeNotifer"/> on <see cref="IChangeNotifer.MODIFIED"/> event.</summary>
/// <param name="ch"></param>
public void reset(object ch) {
if(! isChildOf(child, parent)) return;
//TODO: Handle multi-child parents
content.SetValue(parent, null);
content.SetValue(parent, child);
}
public void track() {
if (isTracking) return;
child.MODIFIED += reset;
}
private bool isChildOf(IChangeNotifer ch, DO p) {
if(ch is DO dch) {
if (H.GetParent(dch) == p) return true;
child.MODIFIED -= reset; isTracking = false;
return false;
}
var chs = H.GetChildren(p);
foreach (var c in chs) if (c == ch) return true;
child.MODIFIED -= reset; isTracking = false;
return false;
}
}
}
public static class DictionaryExtension {
public static V nev<K,V>(this Dictionary<K,V> d, K k, Func<V> c) {
if (d.ContainsKey(k)) return d[k];
var v = c(); d.Add(k, v); return v;
}
}
}
It could be improved and it not fully tested but it works for current purposes.
Additional problem is that some elements like TextBox cry about not suppopring SomeContent, like it is so hard to use ToString()... but that is another story, and is not related to my question.
Updated answer:
I'd throw away implementing SomeContent as a Dependency property and use a UserControl instead:
<UserControl x:Class="WpfApp1.SomeContent"
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"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<TextBlock Text="{Binding some, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SomeContent}}}"/>
</Grid>
</UserControl>
Then in code behind:
/// <summary>
/// Interaction logic for SomeContent.xaml
/// </summary>
public partial class SomeContent : UserControl
{
public static readonly DependencyProperty someProperty =
DependencyProperty.Register(nameof(some), typeof(string),
typeof(SomeContent),
new PropertyMetadata("")
);
public string some
{
get => (string)GetValue(someProperty);
set => SetValue(someProperty, value);
}
public SomeContent()
{
InitializeComponent();
}
}
Next, implement a view model that implements INotifyPropertyChanged:
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _somePropertyOnMyViewModel;
public string SomePropertyOnMyViewModel
{
get => _somePropertyOnMyViewModel;
set { _somePropertyOnMyViewModel = value; OnPropertyChanged(); }
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then create an instance of MyViewModel in your view and assign it to your view's DataContext:
public class MyView : Window
{
public MyView()
{
InitializeComponent();
DataContext = new MyViewModel();
}
}
Then, finally, in MyView use the markup I provided in my original answer:
<Label>
<local:SomeContent x:Name="SomeContent" some="{Binding
SomePropertyOnMyViewModel" />
</Label>

Set up button's IsEnabled property with ICommand [duplicate]

This question already has answers here:
WPF MVVM command canexecute enable/disable button
(4 answers)
Disable button in WPF?
(5 answers)
How does one "disable" a button in WPF using the MVVM pattern?
(5 answers)
Command source disabling and enabling
(1 answer)
Closed 5 years ago.
I have the command
public class RelayActionCommand : ICommand
{
/// <summary>
/// The Action Delegate representing a method with input parameter
/// </summary>
public Action<object> ExecuteAction { get; set; }
/// <summary>
/// The Delegate, used to represent the method which defines criteria for the execution
/// </summary>
public Predicate<object> CanExecuteAction { get; set; }
public bool CanExecute(object parameter)
{
if (CanExecuteAction != null)
{
return CanExecuteAction(parameter);
}
return true;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
if (ExecuteAction != null)
{
ExecuteAction(parameter);
}
}
}
To use it,
public RelayActionCommand SearchPersonCommnad { get; set; }
DataAccess objds;
public PersonViewModel()
{
Persons = new ObservableCollection<PersonInfo>();
objds = new DataAccess();
Persons = new ObservableCollection<PersonInfo>(objds.GetPersonData());
var defaultView = CollectionViewSource.GetDefaultView(Persons);
//based upon the data entered in the TextBox
SearchPersonCommnad = new RelayActionCommand()
{
CanExecuteAction = n=> !String.IsNullOrEmpty(Name),
ExecuteAction = n => defaultView.Filter = name => ((PersonInfo)name).FirstName.StartsWith(Name)
|| ((PersonInfo)name).LastName.StartsWith(Name)
|| ((PersonInfo)name).City==Name
};
At the beginning, the button is disabled. But in running time, it changes by different situations. My question is how to set up the button's IsEnabled property with it? Which means, when ExecuteAction I have to set up the property correctly.
UPDATE:
I use ICommand not DelegateCommand.
You can use the CanExecute method, but it is good practice is actually to avoid this, and bind the button's enabled state to a separate boolean property of the view model. Most other solutions will have unexpected effects, or be suboptimal. Why?
CanExecute is a method. This means that it needs to be polled for the button state to change. You can force the control that's using the command to re-poll on a status change, but the code is much cleaner and more straightforward if you just use a property on the view model. This is because as a method, you can't use INotifyPropertyChanged to notify for changes, whereas with a property you can.
The danger in using CanExecute is that the user will manage to click the button after the method would return false, but before the button's enablement has changed.
Edit: Code to do what you want:
public class ViewModel : INotifyPropertyChanged
{
private int someValue;
private bool isEnabled;
public ViewModel()
{
MyCommand = new RelayActionCommand(Click);
}
private void Click(object obj)
{
//Do something.
}
/// <summary>
/// Bind this to the IsEnabled property of the button, and
/// also the background using a convertor or see ButtonBackground.
/// </summary>
public bool IsEnabled => SomeValue < 5;
/// <summary>
/// Option 2 - use this property to bind to the background of the button.
/// </summary>
public Brush ButtonBackground => IsEnabled ? Brushes.SeaShell : Brushes.AntiqueWhite;
public int SomeValue
{
get { return someValue; }
set
{
if (value == someValue) return;
someValue = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsEnabled));
OnPropertyChanged(nameof(ButtonBackground));
}
}
/// <summary>
/// Bind this to the command of the button.
/// </summary>
public RelayActionCommand MyCommand { get; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged
([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Relay command fixed up a bit to avoid using CanExecute:
public class RelayActionCommand : ICommand
{
public RelayActionCommand(Action<object> executeAction)
{
ExecuteAction = executeAction;
}
/// <summary>
/// The Action Delegate representing a method with input parameter
/// </summary>
public Action<object> ExecuteAction { get; }
/// <summary>
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
ExecuteAction?.Invoke(parameter);
}
//Deliberately empty.
public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
}
EDIT 2: Code to do what you want using a DelegateCommand
Note, this does not use InvalidateRequerySuggested - mainly because it refreshes all buttons when any CanExecute changes, which is a poor solution. As you can see, this is less immediately straightforward than putting the code in the view model directly, but whatever floats your boat I guess.
public sealed class ViewModel : INotifyPropertyChanged
{
private int calls;
public ViewModel()
{
SafeOnceCommand = new RelayCommand(DoItOnce, HasDoneIt);
}
private bool HasDoneIt()
{
return Calls == 0;
}
private void DoItOnce()
{
if (Calls > 0) throw new InvalidOperationException();
Calls++;
}
public int Calls
{
get { return calls; }
set
{
if (value == calls) return;
calls = value;
OnPropertyChanged();
SafeOnceCommand.RaiseCanExecuteChanged();
}
}
public RelayCommand SafeOnceCommand { get; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public sealed class RelayCommand : ICommand
{
private readonly Action execute;
private readonly Func<bool> canExecute;
private readonly List<EventHandler> invocationList = new List<EventHandler>();
public RelayCommand(Action execute, Func<bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute();
}
public void Execute(object parameter)
{
execute();
}
/// <summary>
/// Method to raise CanExecuteChanged event
/// </summary>
public void RaiseCanExecuteChanged()
{
foreach (var elem in invocationList)
{
elem(null, EventArgs.Empty);
}
}
public event EventHandler CanExecuteChanged
{
add { invocationList.Add(value); }
remove { invocationList.Remove(value); }
}
}

WPF INotifyPropertyChanged without burning base class

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"...

Setting root folder of HierarchicalDataTemplate WPF

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

Why can't I reflect a list box choice in a text box in WPF?

I'm new to WPF and I'm having some trouble with my existing setup to get the list box selected item to appear in the text box.
The picture here represents the issue. I typed "12 HOUR" in the text box, which then filters the listbox to those items with "12 HOUR" anywhere in the string. But when I click "12 Hour Nasal" in the list box, I now want to reflect that choice back in the text box:
http://i.imgur.com/ZCYAolT.png
Here is my XAML for the user control containing the listbox and textbox:
<UserControl x:Class="SCM_AllergyRecModule.SearchAndSelectView"
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">
<StackPanel Width="300">
<TextBox x:Name="Filter" Text="{Binding Path=Filter, UpdateSourceTrigger=PropertyChanged}"/>
<ListBox Width ="300" Height="50" x:Name="ListBoxControl"
ItemsSource="{Binding Path=Allergens}"
SelectedItem="{Binding Path=SelectedAllergen}">
</ListBox>
</StackPanel>
And here is the ViewModel:
namespace SCM_AllergyRecModule
{
public class SearchAndSelectViewModel
{
private ICollectionView allergens;
private string selectedAllergen;
private string filter = "";
public string Filter
{
get
{
return this.filter.ToUpperInvariant();
}
set
{
if (this.filter != value)
{
this.filter = value;
this.Allergens.Refresh();
}
}
}
private bool ContainsFilter(object item)
{
var product = item as string;
if (product == null)
{
return false;
}
if (string.IsNullOrEmpty(this.Filter))
{
return true;
}
if (product.ToUpperInvariant().Contains(this.Filter))
{
return true;
}
return false;
}
public SearchAndSelectViewModel()
{
var cvs = new CollectionViewSource();
cvs.Source = MainWindow.scmAllergens;
this.allergens = cvs.View;
this.allergens.Filter = ContainsFilter;
}
public ICollectionView Allergens
{
get
{
return this.allergens;
}
}
public string SelectedAllergen
{
get
{
return this.selectedAllergen;
}
set
{
if (this.selectedAllergen != value)
{
this.selectedAllergen = value;
}
}
}
}
}
Update 1
I added the INotifyPropertyChanged interface to my class and have it being raised on SelectedAllergen in the setter. I added an event handler called SearchAndSelectViewModel_PropertyChanged to handle the SelectedAllergen property changing and set it in the constructor.
Now when I click an item in the listbox, I do see it setting the Filter to the SelectedItem and the list filters to that item so nothing else shows. But still, the text box text is not changing? See screenshot below. This is after I typed in "PEAN" in the textbox, then the listbox filtered to two choices, and I chose "PEANUTS (FOOD)", which then refiltered the list box to just show that choice but didn't set the text box to "PEANUTS (FOOD)":
http://imgur.com/dNxuVI5
Updated ViewModel
public class SearchAndSelectViewModel : INotifyPropertyChanged
{
private ICollectionView allergens;
private string selectedAllergen;
private string filter;
public string Filter
{
get
{
return this.filter.ToUpperInvariant();
}
set
{
if (this.filter != value)
{
this.filter = value;
this.Allergens.Refresh();
}
}
}
private bool ContainsFilter(object item)
{
var product = item as string;
if (product == null)
{
return false;
}
if (string.IsNullOrEmpty(this.Filter))
{
return true;
}
if (product.ToUpperInvariant().Contains(this.Filter))
{
return true;
}
return false;
}
private void SearchAndSelectViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "SelectedAllergen":
this.Filter = this.SelectedAllergen;
break;
}
}
public SearchAndSelectViewModel()
{
filter = "";
var cvs = new CollectionViewSource();
cvs.Source = MainWindow.scmAllergens;
this.allergens = cvs.View;
this.allergens.Filter = ContainsFilter;
this.PropertyChanged += SearchAndSelectViewModel_PropertyChanged;
}
public ICollectionView Allergens
{
get
{
return this.allergens;
}
}
public string SelectedAllergen
{
get
{
return this.selectedAllergen;
}
set
{
if (this.selectedAllergen != value && value != null)
{
this.selectedAllergen = value;
OnPropertyChanged("SelectedAllergen");
}
}
}
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You need to implement the INotifyPropertyChanged interface and you can raise it in your property setter. Since you are also binding your TextBox to the Filter property you need to set the Filter property when your SelectedAllergen changes.
INotifyPropertyChanged example:
public class MyViewModel : INotifyPropertyChanged
{
//...
private int myProperty = 0;
public int MyProperty
{
get { return myProperty; }
set
{
myProperty = value;
// Raise the property changed notification
OnPropertyChanged("MyProperty");
}
}
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if(handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Categories