I am trying to create a SelectedPath property (e.g. in my view-model) that is synchronized with a WPF TreeView. The theory is as follows:
Whenever the selected item in the tree view is changed (SelectedItem property/SelectedItemChanged event), update the SelectedPath property to store a string that represents the whole path to the selected tree node.
Whenever the SelectedPath property is changed, find the tree node indicated by the path string, expand the whole path to that tree node, and select it, after de-selecting the previously selected node.
In order to make all of this reproducible, let us assume that all tree nodes are of type DataNode (see below), that every tree node has a name that is unique among the children of its parent node, and that the path separator be a single forward slash /.
Updating the SelectedPath property in the SelectedItemChange event is not a problem - the following event handler works flawlessly:
void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
DataNode selNode = e.NewValue as DataNode;
if (selNode == null) {
vm.SelectedPath = null;
} else {
vm.SelectedPath = selNode.FullPath;
}
}
However, I fail to make the other way round work properly. Hence, my question, based on the generalized and minimized code sample below, is: How do I make WPF's TreeView respect my programmatical selection of items?
Now, how far have I come? First of all, TreeView's SelectedItem property is read-only, so it cannot be set directly. I have found and read numerous SO questions discussing this in-depth (such as this, this or this), and also resources on other sites, such as this blogpost, this article or this blogpost.
Almost all of these resources point to defining a style for TreeViewItem that binds TreeViewItem's IsSelected property to an equivalent property of the underlying tree node object from the view-model. Sometimes (e.g. here and here), the binding is made two-way, sometimes (e.g. here and here) it's a one-way binding. I don't see the point in making this a one-way-binding (if the tree view UI somehow deselects the item, that change should of course be reflected in the underlying view-model), so I have implemented the two-way version. (The same is usually suggested for IsExpanded, so I have also added a property for that.)
This is the TreeViewItem style I'm using:
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
I have confirmed that this style is actually applied (if I add a setter to set the Background property to Red, all the tree view items do appear with a red background).
And here is the simplified and generalized DataNode class:
public class DataNode : INotifyPropertyChanged
{
public DataNode(DataNode parent, string name)
{
this.parent = parent;
this.name = name;
}
private readonly DataNode parent;
private readonly string name;
public string Name {
get {
return name;
}
}
public override string ToString()
{
return name;
}
public string FullPath {
get {
if (parent != null) {
return parent.FullPath + "/" + name;
} else {
return "/" + name;
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private DataNode[] children;
public IEnumerable<DataNode> Children {
get {
if (children == null) {
children = DataSource.GetChildNodes(FullPath).Select(s => new DataNode(this, s)).ToArray();
}
return children;
}
}
private bool isSelected;
public bool IsSelected {
get {
return isSelected;
}
set {
if (isSelected != value) {
isSelected = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsSelected"));
}
}
}
private bool isExpanded;
public bool IsExpanded {
get {
return isExpanded;
}
set {
if (isExpanded != value) {
isExpanded = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
}
}
}
public void ExpandPath()
{
if (parent != null) {
parent.ExpandPath();
}
IsExpanded = true;
}
}
As you can see, each node has a name, a reference to its parent node (if any), it initializes its child nodes lazily, but only once, and it has an IsSelected and an IsExpanded property, both of which trigger the PropertyChanged event from the INotifyPropertyChanged interface.
So, in my view-model, the SelectedPath property is implemented as follows:
public string SelectedPath {
get {
return selectedPath;
}
set {
if (selectedPath != value) {
DataNode prevSel = NodeByPath(selectedPath);
if (prevSel != null) {
prevSel.IsSelected = false;
}
selectedPath = value;
DataNode newSel = NodeByPath(selectedPath);
if (newSel != null) {
newSel.ExpandPath();
newSel.IsSelected = true;
}
OnPropertyChanged(new PropertyChangedEventArgs("SelectedPath"));
}
}
}
The NodeByPath method correctly (I've checked this) retrieves the DataNode instance for any given path string. Nonetheless, I can run my application and see the following behavior, when binding a TextBox to the SelectedPath property of the view-model:
type /0 => item /0 is selected and expanded
type /0/1/2 => item /0 remains selected, but item /0/1/2 gets expanded.
Similarly, when I first set the selected path to /0/1, that item gets correctly selected and expanded, but for any subsequent path values, the items only get expanded, never selected.
After debugging for a while, I thought the problem was a recursive call of the SelectedPath setter in the prevSel.IsSelected = false; line, but adding a flag that would prevent the execution of the setter code while that command is being executed did not seem to change the behaviour of the programme at all.
So, what am I doing wrong here? I don't see where I'm doing something different than what is suggested in all of those blogposts. Does the TreeView need to be notified somehow about the new IsSelected value of the newly selected item?
For your convencience, the full code of all 5 files that constitute the self-contained, minimal example (the data source obviously returns bogus data in this example, yet it returns a constant tree and hence makes the test cases indicated above reproducible):
DataNode.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
namespace TreeViewTest
{
public class DataNode : INotifyPropertyChanged
{
public DataNode(DataNode parent, string name)
{
this.parent = parent;
this.name = name;
}
private readonly DataNode parent;
private readonly string name;
public string Name {
get {
return name;
}
}
public override string ToString()
{
return name;
}
public string FullPath {
get {
if (parent != null) {
return parent.FullPath + "/" + name;
} else {
return "/" + name;
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private DataNode[] children;
public IEnumerable<DataNode> Children {
get {
if (children == null) {
children = DataSource.GetChildNodes(FullPath).Select(s => new DataNode(this, s)).ToArray();
}
return children;
}
}
private bool isSelected;
public bool IsSelected {
get {
return isSelected;
}
set {
if (isSelected != value) {
isSelected = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsSelected"));
}
}
}
private bool isExpanded;
public bool IsExpanded {
get {
return isExpanded;
}
set {
if (isExpanded != value) {
isExpanded = value;
OnPropertyChanged(new PropertyChangedEventArgs("IsExpanded"));
}
}
}
public void ExpandPath()
{
if (parent != null) {
parent.ExpandPath();
}
IsExpanded = true;
}
}
}
DataSource.cs
using System;
using System.Collections.Generic;
namespace TreeViewTest
{
public static class DataSource
{
public static IEnumerable<string> GetChildNodes(string path)
{
if (path.Length < 40) {
for (int i = 0; i < path.Length + 2; i++) {
yield return (2 * i).ToString();
yield return (2 * i + 1).ToString();
}
}
}
}
}
ViewModel.cs
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
namespace TreeViewTest
{
public class ViewModel : INotifyPropertyChanged
{
private readonly DataNode[] rootNodes = DataSource.GetChildNodes("").Select(s => new DataNode(null, s)).ToArray();
public IEnumerable<DataNode> RootNodes {
get {
return rootNodes;
}
}
private DataNode NodeByPath(string path)
{
if (path == null) {
return null;
} else {
string[] levels = selectedPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
IEnumerable<DataNode> currentAvailable = rootNodes;
for (int i = 0; i < levels.Length; i++) {
string node = levels[i];
foreach (DataNode next in currentAvailable) {
if (next.Name == node) {
if (i == levels.Length - 1) {
return next;
} else {
currentAvailable = next.Children;
}
break;
}
}
}
return null;
}
}
private string selectedPath;
public string SelectedPath {
get {
return selectedPath;
}
set {
if (selectedPath != value) {
DataNode prevSel = NodeByPath(selectedPath);
if (prevSel != null) {
prevSel.IsSelected = false;
}
selectedPath = value;
DataNode newSel = NodeByPath(selectedPath);
if (newSel != null) {
newSel.ExpandPath();
newSel.IsSelected = true;
}
OnPropertyChanged(new PropertyChangedEventArgs("SelectedPath"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null) {
PropertyChanged(this, e);
}
}
}
}
Window1.xaml
<Window x:Class="TreeViewTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TreeViewTest" Height="450" Width="600"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TreeView ItemsSource="{Binding RootNodes}" SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding .}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<TextBox Grid.Row="1" Text="{Binding SelectedPath, Mode=TwoWay}"/>
</Grid>
</Window>
Window1.xaml.cs
using System;
using System.Windows;
namespace TreeViewTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = vm;
}
void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
DataNode selNode = e.NewValue as DataNode;
if (selNode == null) {
vm.SelectedPath = null;
} else {
vm.SelectedPath = selNode.FullPath;
}
}
private readonly ViewModel vm = new ViewModel();
}
}
I could not reproduce the behavior you described. There is a problem with the code you posted unrelated to TreeView. The TextBox default UpdateSourceTrigger is LostFocus therefore the TreeView is affected only after the TextBox loses focus but there are only two controls in your example so to make the TextBox lose focus you have to select something in the TreeView (then the entire selection process is messed up).
What I did was to add a button at the bottom of the form. The button does nothing but when clicked the TextBox loses focus. Everything works perfectly now.
I compiled it in VS2012 using .Net 4.5
Related
I have an existing treeview in WPF in which I would like to add checkboxes
Here the code
I have a class Person which contains all the structure
Person.cs
public class Person
{
readonly List<Person> _children = new List<Person>();
public IList<Person> Children
{
get { return _children; }
}
public string Name { get; set; }
}
As I read in some other posts, I use ViewModel
PersonViewModel.cs
public class PersonViewModel : INotifyPropertyChanged
{
#region Data
readonly ReadOnlyCollection<PersonViewModel> _children;
readonly PersonViewModel _parent;
readonly Person _person;
bool _isExpanded=true;
bool _isSelected;
#endregion Data
#region Constructors
public PersonViewModel(Person person): this(person, null)
{
}
private PersonViewModel(Person person, PersonViewModel parent)
{
_person = person;
_parent = parent;
_children = new ReadOnlyCollection<PersonViewModel>(
(from child in _person.Children
select new PersonViewModel(child, this))
.ToList<PersonViewModel>());
}
#endregion Constructors
#region Person Properties
public ReadOnlyCollection<PersonViewModel> Children
{
get { return _children; }
}
public string Name
{
get { return _person.Name; }
}
#endregion Person Properties
#region Presentation Members
#region IsExpanded
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is expanded.
/// </summary>
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != _isExpanded)
{
_isExpanded = value;
OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (_isExpanded && _parent != null)
_parent.IsExpanded = true;
}
}
#endregion IsExpanded
#region IsSelected
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is selected.
/// </summary>
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
#endregion IsSelected
#region NameContainsText
public bool NameContainsText(string text)
{
if (String.IsNullOrEmpty(text) || String.IsNullOrEmpty(this.Name))
return false;
return Name.IndexOf(text, StringComparison.InvariantCultureIgnoreCase) > -1;
}
#endregion NameContainsText
#region Parent
public PersonViewModel Parent
{
get { return _parent; }
}
#endregion Parent
#endregion Presentation Members
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChanged Members
}
The family tree ViewModel
FamilyTreeViewModel.cs
public class FamilyTreeViewModel
{
#region Data
readonly PersonViewModel _rootPerson;
#endregion Data
#region Constructor
public FamilyTreeViewModel(Person rootPerson)
{
_rootPerson = new PersonViewModel(rootPerson);
FirstGeneration = new ReadOnlyCollection<PersonViewModel>(
new PersonViewModel[]
{
_rootPerson
});
}
#endregion Constructor
#region Properties
#region FirstGeneration
/// <summary>
/// Returns a read-only collection containing the first person
/// in the family tree, to which the TreeView can bind.
/// </summary>
public ReadOnlyCollection<PersonViewModel> FirstGeneration { get; }
#endregion FirstGeneration
#endregion Properties
}
The xaml code
MainWindow.xaml
<TreeView ItemsSource="{Binding FirstGeneration}">
<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}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
readonly FamilyTreeViewModel _familyTree;
public MainWindow()
{
InitializeComponent();
Person rootPerson = new Person
{
Name="Application Architect Right",
Children =
{
new Person
{
Name="Generate"
},
new Person
{
Name="Instances rights",
Children =
{
new Person
{
Name = "Create"
},
new Person
{
Name = "Modify"
},
new Person
{
Name = "Delete"
},
new Person
{
Name = "Exceptions Management"
}
}
},
new Person
{
Name="Templates rights",
Children =
{
new Person
{
Name = "Create"
},
new Person
{
Name = "Modify"
},
new Person
{
Name = "Delete"
}
}
},
new Person
{
Name="Parameters rights",
Children =
{
new Person
{
Name = "Create"
},
new Person
{
Name = "Modify"
},
new Person
{
Name = "Delete"
}
}
},
}
};
// Create UI-friendly wrappers around the
// raw data objects (i.e. the view-model).
_familyTree = new FamilyTreeViewModel(rootPerson);
// Let the UI bind to the view-model.
DataContext = _familyTree;
}
}
Can someone can help me?
Thanks in advance
I'm not sure I fully understand your question, but have you tried editing your XAML like this?
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation=Horizontal>
<TextBlock Text="{Binding Name}" />
<Checkbox IsChecked="{Binding IsSelected} />
</StackPanel>
</HierarchicalDataTemplate>
Since there is no checkbox by default in a treeview in wpf, editing the template of the items to add a checkbox is the way to go.
Since the checkbox is binded to the IsSelected property of your PersonViewModel, you could do something like this if you want to update the selection of the childs
public bool IsSelected
{
...
set
{
if (value != _isSelected)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
this.UpdateChildSelection();
}
}
}
private void UpdateChildSelection()
{
foreach(var child in Children)
{
child.IsSelected = this.IsSelected;
}
}
This is my base ViewModel class for TreeView items, which includes cascading checking.
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 LoadingDataItem { get; }
static perTreeViewItemViewModelBase()
{
LoadingDataItem = new perTreeViewItemViewModelBase { Caption = "Loading Data ..." };
}
private readonly perObservableCollection<perTreeViewItemViewModelBase> _childrenList = new perObservableCollection<perTreeViewItemViewModelBase>();
public perTreeViewItemViewModelBase(bool addLoadingDataItem = false)
{
if (addLoadingDataItem)
_childrenList.Add(LoadingDataItem);
}
private string _caption;
public string Caption
{
get { return _caption; }
set { Set(nameof(Caption), ref _caption, value); }
}
public void ClearChildren()
{
_childrenList.Clear();
}
public void AddChild(perTreeViewItemViewModelBase child)
{
if (_childrenList.Any() && _childrenList.First() == LoadingDataItem)
ClearChildren();
_childrenList.Add(child);
SetChildPropertiesFromParent(child);
}
protected void SetChildPropertiesFromParent(perTreeViewItemViewModelBase child)
{
child.Parent = this;
if (IsChecked.GetValueOrDefault())
child.IsChecked = true;
}
public void AddChildren(IEnumerable<perTreeViewItemViewModelBase> children)
{
foreach (var child in children)
AddChild(child);
}
protected perTreeViewItemViewModelBase Parent { get; private set; }
private bool? _isChecked = false;
public bool? IsChecked
{
get { return _isChecked; }
set
{
if (Set(nameof(IsChecked), ref _isChecked, value))
{
foreach (var child in Children)
if (child.IsEnabled)
child.SetIsCheckedIncludingChildren(value);
SetParentIsChecked();
}
}
}
private bool _isExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (!Set(nameof(IsExpanded), ref _isExpanded, value) || IsInitialised || IsInitialising)
return;
var unused = InitialiseAsync();
}
}
private bool _isEnabled = true;
public bool IsEnabled
{
get { return _isEnabled; }
set { Set(nameof(IsEnabled), ref _isEnabled, value); }
}
public bool IsInitialising { get; private set; }
public bool IsInitialised { get; private set; }
public async Task InitialiseAsync()
{
if (IsInitialised || IsInitialising)
return;
IsInitialising = true;
await InitialiseChildrenAsync().ConfigureAwait(false);
foreach (var child in InitialisedChildren)
SetChildPropertiesFromParent(child);
IsInitialised = true;
RaisePropertyChanged(nameof(Children));
}
protected virtual Task InitialiseChildrenAsync()
{
return Task.CompletedTask;
}
public IEnumerable<perTreeViewItemViewModelBase> Children => IsInitialised
? InitialisedChildren
: _childrenList;
// override this as required in descendent classes
// e.g. if Children is a union of multiple child item collections which are populated in InitialiseChildrenAsync()
protected virtual IEnumerable<perTreeViewItemViewModelBase> InitialisedChildren => _childrenList;
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
// ensure that all ancestor items are expanded, so this item will be visible
if (value)
{
var parent = Parent;
while (parent != null)
{
parent.IsExpanded = true;
parent = parent.Parent;
}
}
if (_isSelected == value)
return;
// use DispatcherPriority.ContextIdle so that we wait for any children of newly expanded items to be fully created in the
// parent TreeView, before setting IsSelected for this item (which will scroll it into view - see perTreeViewItemHelper)
perDispatcherHelper.CheckBeginInvokeOnUI(() => Set(nameof(IsSelected), ref _isSelected, value), DispatcherPriority.ContextIdle);
// 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.
}
}
private void SetIsCheckedIncludingChildren(bool? value)
{
_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));
}
private void SetParentIsChecked()
{
var parent = Parent;
while (parent != null)
{
var hasIndeterminateChild = parent.Children.Any(c => c.IsEnabled && !c.IsChecked.HasValue);
if (hasIndeterminateChild)
parent.SetIsCheckedThisItemOnly(null);
else
{
var hasSelectedChild = parent.Children.Any(c => c.IsEnabled && c.IsChecked.GetValueOrDefault());
var hasUnselectedChild = parent.Children.Any(c => c.IsEnabled && !c.IsChecked.GetValueOrDefault());
if (hasUnselectedChild && hasSelectedChild)
parent.SetIsCheckedThisItemOnly(null);
else
parent.SetIsCheckedThisItemOnly(hasSelectedChild);
}
parent = parent.Parent;
}
}
public override string ToString()
{
return Caption;
}
}
For more details and an example of its usage, see my recent blog post.
Please refer to the Model,ViewModel and View below.
The Textblock control on the view is not getting moved/updated to new position on the screen once i have set the value of the databinding property in the corresponding ViewModel.
Model:
private int _Text_1_Left;
public int Text_1_Left
{
get { return _Text_1_Left; }
set
{
if (_Text_1_Left != value)
{
_Text_1_Left = value;
}
}
}
ViewModel:
public class MyViewModel:INotifyPropertyChanged
{
private int _Text_1_Left = 50;
public int Text_1_Left
{
get { return _Text_1_Left; }
set
{
if (_Text_1_Left != value)
{
_Text_1_Left = value;
RaisePropertyChangedEvent("Text_1_Left");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChangedEvent(string Property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(Property));
}
}
}
View(XAML):
<canvas>
<TextBlock Canvas.Left="{Binding ObjMyVM.Text_1_Left,Mode=OneWay}" Canvas.Top="10" />
</canvas>
View(Xaml.cs)
I am setting the DataBinding property in code behind of View.
public form_load()
{
MyViewModel objMyVM=new MyViewModel()
this.DataContext=objMyVM;
}
private void cmdMoveLeft_MouseUp(object sender, MouseButtonEventArgs e)
{
double Left;
TextBlock lbl_curr_selected;
lbl_curr_selected = Button_Text_1;
if (lbl_curr_selected != null)
{
Left = CurrentRec.Text_1_Left;
Left = Left <= 0 ? 0 : Left - 5;
ObjMyVM.Text_1_Left =int.Parse(Left.ToString()) ;
}
}
No error but textblock is supposed to move left by 5,which it is currently not doing.
Please show me how to move the position of a control element on the UI by changing the binding from code-behind.
As you have set the data context to the object your binding should just be:
Canvas.Left="{Binding Text_1_Left,Mode=OneWay}"
Check the console output for the error.
I'm not sure my Title is right but this is the problem I am facing now.. I have the below XAML code..
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=AvailableFields}"
SelectedItem="{Binding Path=SelectedField}"
></ComboBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
What this basically does is, If my data source contains ten items, this is going to generate 10 row of comboboxes and all comboboxes are bounded to the same itemsource.
Now my requirement is Once an item is selected in the first combo box, that item should not be available in the subsequent combo boxes. How to satisfy this requirement in MVVM and WPF?
This turned out to be harder than I thought when I started coding it. Below sample does what you want. The comboboxes will contain all letters that are still available and not selected in another combobox.
XAML:
<Window x:Class="TestApp.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=SelectedLetters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox
ItemsSource="{Binding Path=AvailableLetters}"
SelectedItem="{Binding Path=Letter}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace TestApp
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
}
public class VM : INotifyPropertyChanged
{
public VM()
{
SelectedLetters = new List<LetterItem>();
for (int i = 0; i < 10; i++)
{
LetterItem letterItem = new LetterItem();
letterItem.PropertyChanged += OnLetterItemPropertyChanged;
SelectedLetters.Add(letterItem);
}
}
public List<LetterItem> SelectedLetters { get; private set; }
private void OnLetterItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "Letter")
{
return;
}
foreach (LetterItem letterItem in SelectedLetters)
{
letterItem.RefreshAvailableLetters(SelectedLetters);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public class LetterItem : INotifyPropertyChanged
{
static LetterItem()
{
_allLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".Select(c => c.ToString());
}
public LetterItem()
{
AvailableLetters = _allLetters;
}
public void RefreshAvailableLetters(IEnumerable<LetterItem> letterItems)
{
AvailableLetters = _allLetters.Where(c => !letterItems.Any(li => li.Letter == c) || c == Letter);
}
private IEnumerable<string> _availableLetters;
public IEnumerable<string> AvailableLetters
{
get { return _availableLetters; }
private set
{
_availableLetters = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableLetters"));
}
}
}
private string _letter;
public string Letter
{
get { return _letter; }
set
{
if (_letter == value)
{
return;
}
_letter = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Letter"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private static readonly IEnumerable<string> _allLetters;
}
}
}
This functionality is not provided by WPF, but it can be implemented using some custom coding.
I've created 3 ViewModel classes:
PreferencesVM - This will be our DataContext. It contains the master list of options which can appear in the ComboBoxes, and also contains a SelectedOptions property, which keeps track of which items are selected in the various ComboBoxes. It also has a Preferences property, which we will bind our ItemsControl.ItemsSource to.
PreferenceVM - This represents one ComboBox. It has a SelectedOption property, which ComboBox.SelectedItem is bound to. It also has a reference to PreferencesVM, and a property named Options (ComboBox.ItemsSource is bound to this), which returns the Options on PreferencesVM via a filter which checks if the item may be displayed in the ComboBox.
OptionVM - Represents a row in the ComboBox.
The following points form the key to the solution:
When PreferenceVM.SelectedOption is set (ie a ComboBoxItem is selected), the item is added to the PreferencesVM.AllOptions collection.
PreferenceVM handles Preferences.SelectedItems.CollectionChanged, and triggers a refresh by raising PropertyChanged for the Options property.
PreferenceVM.Options uses a filter to decide which items to return - which only allows items which are not in PreferencesVM.SelectedOptions, unless they are the SelectedOption.
What I've described above might be enough to get you going, but to save you the headache I'll post my code below.
PreferencesVM.cs:
public class PreferencesVM
{
public PreferencesVM()
{
PreferenceVM pref1 = new PreferenceVM(this);
PreferenceVM pref2 = new PreferenceVM(this);
PreferenceVM pref3 = new PreferenceVM(this);
this._preferences.Add(pref1);
this._preferences.Add(pref2);
this._preferences.Add(pref3);
//Only three ComboBoxes, but you can add more here.
OptionVM optRed = new OptionVM("Red");
OptionVM optGreen = new OptionVM("Green");
OptionVM optBlue = new OptionVM("Blue");
_allOptions.Add(optRed);
_allOptions.Add(optGreen);
_allOptions.Add(optBlue);
}
private ObservableCollection<OptionVM> _selectedOptions =new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> SelectedOptions
{
get { return _selectedOptions; }
}
private ObservableCollection<OptionVM> _allOptions = new ObservableCollection<OptionVM>();
public ObservableCollection<OptionVM> AllOptions
{
get { return _allOptions; }
}
private ObservableCollection<PreferenceVM> _preferences = new ObservableCollection<PreferenceVM>();
public ObservableCollection<PreferenceVM> Preferences
{
get { return _preferences; }
}
}
PreferenceVM.cs:
public class PreferenceVM:INotifyPropertyChanged
{
private PreferencesVM _preferencesVM;
public PreferenceVM(PreferencesVM preferencesVM)
{
_preferencesVM = preferencesVM;
_preferencesVM.SelectedOptions.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedOptions_CollectionChanged);
}
void SelectedOptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this,new PropertyChangedEventArgs("Options"));
}
private OptionVM _selectedOption;
public OptionVM SelectedOption
{
get { return _selectedOption; }
set
{
if (value == _selectedOption)
return;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Remove(_selectedOption);
_selectedOption = value;
if (_selectedOption != null)
_preferencesVM.SelectedOptions.Add(_selectedOption);
}
}
private ObservableCollection<OptionVM> _options = new ObservableCollection<OptionVM>();
public IEnumerable<OptionVM> Options
{
get { return _preferencesVM.AllOptions.Where(x=>Filter(x)); }
}
private bool Filter(OptionVM optVM)
{
if(optVM==_selectedOption)
return true;
if(_preferencesVM.SelectedOptions.Contains(optVM))
return false;
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
}
OptionVM.cs:
public class OptionVM
{
private string _name;
public string Name
{
get { return _name; }
}
public OptionVM(string name)
{
_name = name;
}
}
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new PreferencesVM();
}
}
MainWindow.xaml:
<Window x:Class="WpfApplication64.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ItemsControl ItemsSource="{Binding Path=Preferences}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding Path=Options}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedOption}"></ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
**Note that to reduce lines of code, my provided solution only generates 3 ComboBoxes (not 10).
I'm triying to bind to a RadioButton.IsChecked property, and it only works once. After that, the binding doesn't work anyore, and I have no idea why this happens. Can anyone help out with this? Thanks!
This is my code.
C#
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
public class ViewModel
{
private bool _isChecked1 = true;
public bool IsChecked1
{
get { return _isChecked1; }
set
{
if (_isChecked1 != value)
{
_isChecked1 = value;
}
}
}
private bool _isChecked2;
public bool IsChecked2
{
get { return _isChecked2; }
set
{
if (_isChecked2 != value)
{
_isChecked2 = value;
}
}
}
}
XAML:
<Grid>
<StackPanel>
<RadioButton Content="RadioButton1" IsChecked="{Binding IsChecked1}" />
<RadioButton Content="RadioButton2" IsChecked="{Binding IsChecked2}" />
</StackPanel>
</Grid>
It's an unfortunate known bug. I'm assuming this has been fixed in WPF 4.0 given the new DependencyObject.SetCurrentValue API, but have not verified.
Here is a working solution: http://pstaev.blogspot.com/2008/10/binding-ischecked-property-of.html. It's a shame that Microsoft didn't correct this error.
Just a follow-up to Kent's answer here...this has in fact been fixed in WPF 4.0., I'm leveraging this behavior in my current project. The radio button that is de-activated now gets its binding value set to false, rather than breaking the binding.
I guess you need to implement the INotifyPropertyChanged interface
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private bool _isChecked1 = true;
public bool IsChecked1
{
get { return _isChecked1; }
set
{
if (_isChecked1 != value)
{
_isChecked1 = value;
NotifyPropertyChanged("IsChecked1");
}
}
} // and the other property...
:)
ViewModel has 2 field. Name, Childs
I need like this
1. When click on the root element, do 2 operation
first. expand yourself
second. select first child. If child element has childs, repeat 1.
otherwise do nothing
Only last child (leaf) selectable
UPDATE
Figured out a much better way to do this. This will also account for changes in the ObservableCollection.
The Xaml can just look like this
<Window.Resources>
<HierarchicalDataTemplate x:Key="Level1"
ItemsSource="{Binding Path=Childs}">
<TextBlock Text="{Binding Path=Name}"/>
</HierarchicalDataTemplate>
</Window.Resources>
<TreeView ItemsSource="{Binding}"
...>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
And then we handle the IsSelected Property in the Model/ViewModel instead.
public class MyViewModel : INotifyPropertyChanged
{
private static MyViewModel s_lastSelectedTestItem = null;
public MyViewModel(string name)
{
Name = name;
m_isSelected = false;
Childs = new ObservableCollection<MyViewModel>();
Childs.CollectionChanged += new NotifyCollectionChangedEventHandler(TestItems_CollectionChanged);
}
void TestItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (IsSelected == true && Childs.Count > 0)
{
Childs[0].IsSelected = true;
IsExpanded = true;
}
}
public string Name
{
get;
set;
}
public ObservableCollection<MyViewModel> Childs
{
get;
set;
}
private bool m_isSelected;
public bool IsSelected
{
get
{
return m_isSelected;
}
set
{
m_isSelected = value;
if (m_isSelected == true)
{
if (s_lastSelectedTestItem != null)
{
s_lastSelectedTestItem.IsSelected = false;
}
s_lastSelectedTestItem = this;
if (Childs.Count > 0)
{
IsExpanded = true;
Childs[0].IsSelected = true;
m_isSelected = false;
}
}
OnPropertyChanged("IsSelected");
}
}
private bool m_isExpaned;
public bool IsExpanded
{
get
{
return m_isExpaned;
}
set
{
m_isExpaned = value;
OnPropertyChanged("IsExpanded");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
What about to capture select item event on the treeview and expand the first child of the selected item? It seems easy to do.