WPF MVVM TreeView using HierarchicalDataTemplate not updating - c#

So I've been struggling with getting my TreeViews to update properly for a long time now and so I'm asking if anyone can tell me why my code isn't properly updating my TreeView nodes on additions or subtractions. I apologize in advance for the somewhat massive code dump but I felt it was all important to illustrate the problem.
For starters my ObservableObject class
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
TreeNodeBase Class
public abstract class TreeNodeBase : ObservableObject
{
protected const string ChildNodesPropertyName = "ChildNodes";
protected string name;
public string Name
{
get
{
return this.name;
}
set
{
this.name = value;
this.OnPropertyChanged();
}
}
protected IList<TreeNode> childNodes;
protected TreeNodeBase(string name)
{
this.Name = name;
this.childNodes = new List<TreeNode>();
}
public IEnumerable<TreeNode> ChildNodes
{
get
{
return this.childNodes;
}
}
public TreeNodeBase AddChildNode(string name)
{
var treeNode = new TreeNode(this, name);
this.childNodes.Add(treeNode);
this.OnPropertyChanged(ChildNodesPropertyName);
return treeNode;
}
public TreeNode RemoveChildNode(string name)
{
var nodeToRemove = this.childNodes.FirstOrDefault(node => node.Name.Equals(name));
if (nodeToRemove != null)
{
this.childNodes.Remove(nodeToRemove);
this.OnPropertyChanged(ChildNodesPropertyName);
}
return nodeToRemove;
}
}
public class TreeNode : TreeNodeBase
{
public TreeNodeBase Parent { get; protected set; }
public TreeNode(TreeNodeBase parent, string name)
: base(name)
{
this.Parent = parent;
}
}
The TreeNodeRoot class
public class TreeViewRoot : TreeNodeBase
{
public TreeViewRoot(string name)
: base(name)
{
}
}
The TreeNode Class
public class TreeNode : TreeNodeBase
{
public TreeNodeBase Parent { get; protected set; }
public TreeNode(TreeNodeBase parent, string name)
: base(name)
{
this.Parent = parent;
}
}
The TreeView UserControl Xaml
<UserControl x:Class="TreeViewExperiment.TreeView"
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:treeViewExperiment="clr-namespace:TreeViewExperiment"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400"
d:DataContext="{d:DesignInstance treeViewExperiment:TreeViewmodel}">
<UserControl.DataContext>
<treeViewExperiment:TreeViewmodel/>
</UserControl.DataContext>
<Grid Background="White">
<Grid.Resources>
<HierarchicalDataTemplate x:Key="TreeViewHierarchicalTemplate" ItemsSource="{Binding ChildNodes}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
<Style TargetType="Button">
<Setter Property="FontFamily" Value="Verdana"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="6*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TreeView Grid.Row="0" x:Name="Tree" ItemsSource="{Binding RootLevelNodes}" ItemTemplate="{StaticResource TreeViewHierarchicalTemplate}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectedItemChanged">
<i:InvokeCommandAction
Command="{Binding SetSelectedNode}"
CommandParameter="{Binding SelectedItem, ElementName=Tree}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
<Grid Grid.Row="1" Height="25">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="NameTextBox" Grid.Column="0" VerticalAlignment="Center" FontFamily="Verdana"/>
<Button Grid.Column="1" Content="Add Node" Command="{Binding AddNode}" CommandParameter="{Binding Text, ElementName=NameTextBox}" Background="Green"/>
<Button Grid.Column="2" Content="Remove Node" Command="{Binding RemoveNode}" Background="Red"/>
</Grid>
</Grid>
</UserControl>
Finally the TreeViewmodel
public class TreeViewmodel : ObservableObject
{
public ICommand SetSelectedNode { get; private set; }
public ICommand AddNode { get; private set; }
public ICommand RemoveNode { get; private set; }
public TreeViewmodel()
{
this.SetSelectedNode = new ParamaterizedDelegateCommand(
node =>
{
this.SelectedTreeNode = (TreeNodeBase)node;
});
this.AddNode = new ParamaterizedDelegateCommand(name => this.SelectedTreeNode.AddChildNode((string)name));
this.RemoveNode = new DelegateCommand(
() =>
{
if (selectedTreeNode.GetType() == typeof(TreeNode))
{
var parent = ((TreeNode)this.SelectedTreeNode).Parent;
parent.RemoveChildNode(this.SelectedTreeNode.Name);
this.SelectedTreeNode = parent;
}
});
var adam = new TreeViewRoot("Adam");
var steve = adam.AddChildNode("Steve");
steve.AddChildNode("Jack");
this.rootLevelNodes = new List<TreeViewRoot> { adam, new TreeViewRoot("Eve") };
}
private TreeNodeBase selectedTreeNode;
private readonly IList<TreeViewRoot> rootLevelNodes;
public IEnumerable<TreeViewRoot> RootLevelNodes
{
get
{
return this.rootLevelNodes;
}
}
public TreeNodeBase SelectedTreeNode
{
get
{
return this.selectedTreeNode;
}
set
{
this.selectedTreeNode = value;
this.OnPropertyChanged();
}
}
}
So I know that the UI should be getting notified when child elements are added removed as when I debug it I can see that the get accessor on the ChildNodes property is called in both cases, yet what is displayed on the UI remains unchanged.
In the past I've gotten around this but using ObservableCollections and that seems to be what most solutions to this sort of problem point to here on StackOverflow, but why doesn't this solution also work? What am I missing?

The problem is that you are misusing INotifyPropertyChanged. In your code you are notifying the view that your property ChildNodes changed but it isn't true as TreeViewItem.ItemsSource still equals your ChildNodes property.
INotifyPropertyChanged will cause UI update when underlying collection object in yout view model changes.
To get ItemsSource updated when new item in collection occurs you need to use a collection which implements INotifyCollectionChanged.
As MSDN says:
You can enumerate over any collection that implements the IEnumerable interface. However, to set up dynamic bindings so that insertions or deletions in the collection update the UI automatically, the collection must implement the INotifyCollectionChanged interface. This interface exposes an event that should be raised whenever the underlying collection changes.
That's why everyone on SO advise to use ObservableCollection.
EDIT:
If you want to expose read-only collection you should check ReadOnlyObservableCollection<T> Class. It works as a wrapper for ObservableCollection which can be made non public.

Related

Binding a Stack (that can be switched) to a ListBox

I have a list of objects (Man) which each contains a Stack of states.
I have a Debug window which shows the selected Man's stack in a ListBox.
And I have a TabControl which I use to select a Man to debug.
To be able to select the correct binding, I made a property which returns the StateStack of the man at the selected index of the TabControl.
public object StateStack => Men[DebugIndex].States;
DebugIndex is bound to the TabControl's SelectedIndex property. So to make DebugIndex update the StateStack to show, I used OnPropertyChanged:
public int DebugIndex {
get => _debugIndex;
set {
_debugIndex = value;
OnPropertyChanged(nameof(StateStack));
}
}
The problem is, when the TabControl's SelectedIndex changes, the Stack is weirdly disordered! Bug the thing is that it's disordered only in the View, not really in the data.
I think it comes from something with the fact that I change the reference of the Binding it's an other Stack but I don't know how to solve that...
By the way, it works when I add all the Man objects and initialize their StateStack at the beginning. But as soon as I add a Man (and initialize its StateStack) later, for example when I click a Button, it doesn't work anymore...
public sealed partial class MainWindow : INotifyPropertyChanged {
private int _debugIndex;
public ObservableCollection<Man> Men { get; } = new ObservableCollection<Man>();
public MainWindow() {
Men.Add(new Man {Index = 0, States = new StateStack()});
InitializeComponent();
Men[0].States.Push(new State {Name = "Falling1"});
Men[0].States.Push(new State {Name = "Walking1"});
//this is simplified code. I push states here because in my program it's done during runtime (not during initialization)
}
public object StateStack => Men[DebugIndex].States;
public int DebugIndex {
get => _debugIndex;
set {
_debugIndex = value;
OnPropertyChanged(nameof(StateStack));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e) {
Men.Add(new Man {Index = 1, States = new StateStack()});
Men[1].States.Push(new State {Name = "Falling2"});
Men[1].States.Push(new State {Name = "Walking2"});
Men[1].States.Push(new State {Name = "Running2"});
}
}
public class Man {
public int Index { get; set; }
public StateStack States { get; set; }
}
public class State {
public string Name { private get; set; }
public override string ToString() {
return Name;
}
}
public sealed class StateStack : Stack<State>, INotifyCollectionChanged {
public new void Push(State item) {
base.Push(item);
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, Count - 1));
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) {
CollectionChanged?.Invoke(this, e);
}
}
And my View code:
<Window x:Class="ObservableStackBug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Content="Add" Margin="5" Padding="8 2" HorizontalAlignment="Left" Click="ButtonBase_OnClick"/>
<ListBox ItemsSource="{Binding StateStack}" Grid.Row="1" />
<TabControl Grid.Row="2" ItemsSource="{Binding Men}" SelectedIndex="{Binding DebugIndex}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Index}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
</Window>
What could I do to say to my binding that when DebugIndex is changed, StateStack is a very other Stack?
I've simulated your scenario and observation is that there is problem with Push method for how the NotifyCollectionChangedEventArgs is item changed is propagated to source. The current code notifies that items are changed from the end index (but for stack the items are added at Top)). If you update the notification start index to 0 as NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0) then bound source will display the item in appropriate order in the view. You can read about NotifyCollectionChangedEventArgs here.
public new void Push(State item) {
base.Push(item);
OnCollectionChanged(
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, 0));
}

WPF Sorting an ObservableCollection de-selects a ComboBox

I have a ComboBox where a user can select what JobType they are working on. The ComboBox has a list of AllJobTypes. The problem stems from when a user adds a new JobType, I add the JobType to the AllJobTypes ObservableCollection, then sort it. When the sorting happens the ComboBox get's de-selected and not really sure why. The JobConfig.SelectedJobType.Name never changes in this process. Is there a way to sort an observable collection where it doesn't break the ComboBox?
public class JobTypeList : ObservableCollection<JobType> {
public void SortJobTypes() {
var sortableList = new List<JobType>(this);
sortableList.Sort((x, y) => x.Name.CompareTo(y.Name));
//this works but it creates a bug in the select for JobTypes
for (int i = 0; i < sortableList.Count; i++) {
this.Move(this.IndexOf(sortableList[i]), i);
}
}
And in the XAML
<ComboBox Grid.Column="0" SelectionChanged="JobTypeComboBox_SelectionChanged"
Name="JobTypeComboBox"
ItemsSource="{Binding Path=AllJobTypes}"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedValue="{Binding
Path=JobConfig.SelectedJobType.Name}" />
Instead of sorting the collection in the view model, you should bind the ComboBox's ItemsSource to a CollectionViewSource, where you can specify a SortDescription:
<Window ...
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
...>
<Window.Resources>
<CollectionViewSource x:Key="cvs" Source="{Binding AllJobTypes}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Name"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
...
<ComboBox ItemsSource="{Binding Source={StaticResource cvs}}"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedValue="{Binding JobConfig.SelectedJobType.Name}"/>
...
</Window>
For further information see How to: Sort and Group Data Using a View in XAML
Here's a version using ItemsSource/SelectedItem. Note that you can add a new item to the list and sort it without losing the currently selected item in the view.
The window
<Window
x:Class="SortingAList.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:SortingAList"
mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBox
Text="{Binding NewJobType, Delay=1000}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="200" />
<ComboBox
Grid.Row="1"
ItemsSource="{Binding JobTypes}"
SelectedItem="{Binding SelectedJobType}"
DisplayMemberPath="Name"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="200" />
</Grid>
</Window>
The code
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class Notifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Notify([CallerMemberName]string property = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
public class ViewModel : Notifier
{
private JobType _selectedJobType;
private string _newJobType;
public JobTypeList JobTypes { get; private set; }
public JobType SelectedJobType { get => _selectedJobType; set { _selectedJobType = value; Notify(); } }
public string NewJobType { get => _newJobType; set { _newJobType = value; Notify(); AddNewJobType(value); } }
public ViewModel()
{
JobTypes = new JobTypeList();
JobTypes.Add(new JobType { Name = "Butcher" });
JobTypes.Add(new JobType { Name = "Baker" });
JobTypes.Add(new JobType { Name = "LED maker" });
}
private void AddNewJobType(string name)
{
if(JobTypes.Any(x => x.Name == name)) return;
JobTypes.Add(new JobType { Name = name });
JobTypes.SortJobTypes();
}
}
public class JobType : Notifier
{
private string _name;
public string Name { get => _name; set { _name = value; Notify(); } }
}
Using your JobTypesList
public class JobTypeList : ObservableCollection<JobType>
{
public void SortJobTypes()
{
var sortableList = new List<JobType>(this);
sortableList.Sort((x, y) => x.Name.CompareTo(y.Name));
//this works but it creates a bug in the select for JobTypes
for(int i = 0; i < sortableList.Count; i++)
{
this.Move(this.IndexOf(sortableList[i]), i);
}
}
}

Why are my User Settings saved only the first time this method is called?

I have a WPF MVVM app, which gets its data from a user setting which is an ObservableCollection of type Copyable (a custom class) called Copyables. Within the main view model (ClipboardAssistantViewModel), I set the source of a CollectionViewSource to Copyables. This is then bound to an ItemsControl in the main view (MainWindow). The DataTemplate for this ItemsControl is a user control, 'CopyableControl', which is essentially a button, but with attached properties that allow me to bind data and commands to it.
When a user clicks on a CopyableControl, a view model (DefineCopyableViewModel) is added to an ObservableCollection of those in ClipboardAssistantViewModel, and that collection is bound to an ItemsControl in MainWindow. The DataTemplate of this is a UserControl called DefineCopyableControl, which is set up in such a way that the current values associated with the clicked Copyable are bound to textboxes in the DefineCopyableControl for editing.
My problem: There is a method in DefineCopyableViewModel, EditCopyable(), which only works on the first run (its job is to save the user settings once any edits have taken place and the user clicks "OK"). If I click the CopyableControl and make an edit, then click "OK", then click it again, make another edit, then click "OK", then close the application and open it again, only the first edit has been saved (even though the UI was updated with the edited value both times). It seems to have something to do with the data-binding need to be "refreshed"; please see the comments in this method in the code for my findings around this.
My code is as follows:
Model:
namespace ClipboardAssistant.Models
{
public class Copyable : INotifyPropertyChanged
{
// INotifyPropertyChanged implementation
private string name;
public string Name
{
get { return name; }
set
{
if (value != name)
{
name = value;
NotifyPropertyChanged("Name");
}
}
}
private string textToCopy;
public string TextToCopy
{
get { return textToCopy; }
set
{
if (value != textToCopy)
{
textToCopy = value;
NotifyPropertyChanged("TextToCopy");
}
}
}
public Copyable() { }
public Copyable(string Name, string TextToCopy)
{
this.Name = Name;
this.TextToCopy = TextToCopy;
}
}
}
ViewModels:
namespace ClipboardAssistant.ViewModels
{
public class ClipboardAssistantViewModel : INotifyPropertyChanged
{
// INotifyPropertyChanged Implementation
public CollectionViewSource CopyablesView { get; set; }
public ObservableCollection<DefineCopyableViewModel> Definers { get; set; }
public CopyableClickCommand CopyableClickCommand { get; set; }
public ClipboardAssistantViewModel()
{
Definers = new ObservableCollection<DefineCopyableViewModel>();
CopyablesView = new CollectionViewSource();
CopyablesView.Source = Properties.Settings.Default.Copyables;
CopyableClickCommand = new CopyableClickCommand(this);
EditModeClickCommand = new EditModeClickCommand(this);
}
public void RefreshCopyables()
{
// Both these methods of refreshing appear to have the same effect.
Properties.Settings.Default.Copyables = (ObservableCollection<Copyable>)CopyablesView.Source;
CopyablesView.Source = Properties.Settings.Default.Copyables;
}
public void EditCopyable(Copyable Copyable)
{
Definers.Add(new DefineCopyableViewModel(Copyable, this));
}
}
}
namespace ClipboardAssistant.ViewModels
{
public class DefineCopyableViewModel : INotifyPropertyChanged
{
// INotifyPropertyChanged Implementation
public ClipboardAssistantViewModel MyParent { get; set; }
public Copyable Copyable { get; set; }
public DefinerOKClickCommand DefinerOKClickCommand { get; set; }
public DefineCopyableViewModel(Copyable Copyable, ClipboardAssistantViewModel MyParent)
{
this.Copyable = Copyable;
this.MyParent = MyParent;
DefinerOKClickCommand = new DefinerOKClickCommand(this);
}
public void EditCopyable()
{
// Refresh, save, no refresh, save -> doesn't save second edit.
// Save, refresh, save, no refresh -> does save second edit.
MessageBoxResult r = MessageBox.Show("Refresh?", "Refresh", MessageBoxButton.YesNo);
if (r == MessageBoxResult.Yes)
{
MyParent.RefreshCopyables();
}
// These two MessageBox methods (save and refresh) can be swapped around (see above comments).
MessageBoxResult s = MessageBox.Show("Save?", "Save", MessageBoxButton.YesNo);
if (s == MessageBoxResult.Yes)
{
Properties.Settings.Default.Save();
}
MyParent.Definers.Remove(this);
}
}
}
MainWindow:
<Window x:Class="ClipboardAssistant.Views.MainWindow" x:Name="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:vm="clr-namespace:ClipboardAssistant.ViewModels"
xmlns:ctrls="clr-namespace:ClipboardAssistant.Controls"
mc:Ignorable="d"
Title="Clipboard Assistant" Height="400" Width="700">
<Window.DataContext>
<vm:ClipboardAssistantViewModel />
</Window.DataContext>
<Grid>
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="20" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding CopyablesView.View}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ctrls:CopyableControl Copyable="{Binding}"
ClickCopyable="{Binding DataContext.CopyableClickCommand, ElementName=mainWindow}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<DockPanel Grid.Row="2">
<Button x:Name="btnEditCopyableMode" HorizontalAlignment="Left" DockPanel.Dock="Left"
Content="Edit" Margin="0,0,10,0" Command="{Binding EditModeClickCommand}" />
</DockPanel>
</Grid>
<ItemsControl ItemsSource="{Binding Definers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ctrls:DefineCopyableControl Copyable="{Binding DataContext.Copyable}"
ClickCancel="{Binding DataContext.DefinerCancelClickCommand, ElementName=mainWindow}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
CopyableControl:
<UserControl x:Class="ClipboardAssistant.Controls.CopyableControl" x:Name="copyableControl"
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:ClipboardAssistant.Controls"
mc:Ignorable="d" d:DesignHeight="75" d:DesignWidth="200">
<Grid Width="200" Height="75">
<Button Command="{Binding ClickCopyable, ElementName=copyableControl}"
CommandParameter="{Binding Copyable, ElementName=copyableControl}"
Content="{Binding Copyable.Name, ElementName=copyableControl}"
Style="{StaticResource CopyableMainButtonStyle}" />
</Grid>
</UserControl>
namespace ClipboardAssistant.Controls
{
public partial class CopyableControl : UserControl
{
public static readonly DependencyProperty ClickCopyableProperty =
DependencyProperty.Register("ClickCopyable", typeof(ICommand), typeof(CopyableControl));
public ICommand ClickCopyable
{
get { return (ICommand)GetValue(ClickCopyableProperty); }
set { SetValue(ClickCopyableProperty, value); }
}
public static readonly DependencyProperty CopyableProperty =
DependencyProperty.Register("Copyable", typeof(Copyable), typeof(CopyableControl));
public Copyable Copyable
{
get { return (Copyable)GetValue(CopyableProperty); }
set { SetValue(CopyableProperty, value); }
}
public CopyableControl()
{
InitializeComponent();
}
}
}
DefineCopyableControl:
<UserControl x:Class="ClipboardAssistant.Controls.DefineCopyableControl" x:Name="defineCopyableControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="500">
<Grid x:Name="MainGrid" Background="Blue">
<Grid Width="200" Height="180">
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="10" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="20" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="Name" Foreground="White" />
<TextBox Grid.Row="1" Text="{Binding Copyable.Name}" x:Name="tbN" />
<Label Grid.Row="3" Content="Copyable Text" Foreground="White" />
<TextBox Grid.Row="4" Text="{Binding Copyable.TextToCopy}" x:Name="tbTTC" />
<DockPanel Grid.Row="6">
<Button Width="70" Content="OK" DockPanel.Dock="Right" HorizontalAlignment="Right"
Command="{Binding DefinerOKClickCommand}"
CommandParameter="{Binding ElementName=defineCopyableControl}" />
</DockPanel>
</Grid>
</Grid>
</UserControl>
public partial class DefineCopyableControl : UserControl
{
public static readonly DependencyProperty CopyableProperty =
DependencyProperty.Register("Copyable", typeof(Copyable), typeof(DefineCopyableControl));
public Copyable Copyable
{
get { return (Copyable)GetValue(CopyableProperty); }
set { SetValue(CopyableProperty, value); }
}
public DefineCopyableControl()
{
InitializeComponent();
}
}
Commands:
namespace ClipboardAssistant.ViewModels.Commands
{
public class CopyableClickCommand : ICommand
{
public ClipboardAssistantViewModel ViewModel { get; set; }
public CopyableClickCommand(ClipboardAssistantViewModel viewModel)
{
ViewModel = viewModel;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
Copyable cp = (Copyable)parameter;
ViewModel.EditCopyable(cp);
}
}
}
namespace ClipboardAssistant.ViewModels.Commands
{
public class DefinerOKClickCommand : ICommand
{
public DefineCopyableViewModel ViewModel { get; set; }
public DefinerOKClickCommand(DefineCopyableViewModel viewModel)
{
ViewModel = viewModel;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
ViewModel.EditCopyable();
}
}
}
Settings:
namespace ClipboardAssistant.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public global::System.Collections.ObjectModel.ObservableCollection<ClipboardAssistant.Models.Copyable> Copyables {
get {
return ((global::System.Collections.ObjectModel.ObservableCollection<ClipboardAssistant.Models.Copyable>)(this["Copyables"]));
}
set {
this["Copyables"] = value;
}
}
}
}
I'm assuming you are using Visual Studio. In that case, in the My Project do you have the settings listed in the settings tab?
I ran into the same issue a while back where I tried to programatically create/save/update settings and was unsucessful until I created the setting in the Settings tab. Once that was complete I was able to make my saves as necessary.
The you just use
MySettings.Default.SettingName = value
MySettings.Default.Save()
Hope this helps!

Multiple ListBoxes SelectedItem Binding to same property

I have two listboxes that both contain the same type of items and I have both their SelectedItem Property bound to the same Property on my ViewModel. I expected that, when I selected some item in List A, the Selection in List B disappears. But that is not the case. I need an ugly workaround to make it work how I expect it. Any hints how to make it work without this workaround?
XAML
<Window x:Class="WpfApplication1.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" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ListBox ItemsSource="{Binding Items1}" SelectedItem="{Binding SelectedItem}" ></ListBox>
<ListBox Grid.Column="1" ItemsSource="{Binding Items2}" SelectedItem="{Binding SelectedItem}" ></ListBox>
<Button Grid.Row="1" Content="NULL" Grid.ColumnSpan="2" Click="ButtonBase_OnClick"></Button>
</Grid>
</Window>
C#
public partial class MainWindow : INotifyPropertyChanged
{
private Item _selectedItem;
public ObservableCollection<Item> Items1 { get; private set; }
public ObservableCollection<Item> Items2 { get; private set; }
public Item SelectedItem
{
get { return _selectedItem; }
set
{
// Uncomment this to make it work
//_selectedItem = null;
//OnPropertyChanged();
_selectedItem = value;
OnPropertyChanged();
}
}
public MainWindow()
{
Items1 = new ObservableCollection<Item>();
Items2 = new ObservableCollection<Item>();
Items1.Add(new Item("A1"));
Items1.Add(new Item("A2"));
Items2.Add(new Item("B1"));
Items2.Add(new Item("B2"));
InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
SelectedItem = null;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Item
{
public Item(string name)
{
Name = name;
}
string Name { get; set; }
}
Why not binding them (TwoWay binding) to two different Properties (e.g. SelectedItem1 and SelectedItem2), Then:
public Item SelectedItem1
{
get { return _selectedItem1; }
set
{
_selectedItem1 = value;
OnPropertyChanged();
if(value!=null) //Here is the trick.
SelectedItem2=null;
OnPropertyChanged("SelectedItem");
}
}
EDIT: Then for the rest of your application code you can have a SelectedItem property like below. Also don't forget to add OnPropertyChanged("SelectedItem") in setter of both SelectedItem1 and SelectedItem2
public Item SelectedItem
{
get{return SelectedItem1 != null ? SelectedItem1 : SelectedItem2}
}
If it's an ugly workaround you want then tap the SelectionChanged event:
<ListBox x:Name="ListBoxA" ItemsSource="{Binding Items1}" SelectedItem="{Binding SelectedItem}" SelectionChanged="ListBox_SelectionChanged"></ListBox>
<ListBox x:Name="ListBoxB" Grid.Column="1" ItemsSource="{Binding Items2}" SelectedItem="{Binding SelectedItem}" SelectionChanged="ListBox_SelectionChanged" ></ListBox>
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender == this.ListBoxA)
this.ListBoxB.UnselectAll();
else
this.ListBoxA.UnselectAll();
}
Better yet, use an attached behaviour to do the same thing.

WPF: Binding a List<class> to a ComboBox

I've been working on this problem for about 3 hours now, and I got to a dead end.
Currently I'm trying to bind a list to a ComboBox.
I have used several methods to bind the List:
Code behind:
public partial class MainWindow : Window
{
public coImportReader ir { get; set; }
public MainWindow()
{
ir = new coImportReader();
InitializeComponent();
}
private void PremadeSearchPoints(coSearchPoint sp)
{
//SearchRefPoint.DataContext = ir.SearchPointCollection;
SearchRefPoint.ItemsSource = ir.SearchPointCollection;
SearchRefPoint.DisplayMemberPath = Name;
The data was binded correctly but the DisplayMemeberPath for some reason returned the name of the class and not the name of it's member.
The XAML method returned an empty string...
<ComboBox x:Name="SearchRefPoint" Height="30" Width="324" Margin="0,10,0,0"
VerticalAlignment="Top" ItemsSource="{Binding ir.SearchPointCollection}"
DisplayMemberPath="Name">
I've also tried to fill it with a new list which I create in the MainWindow. the result was the same in both cases.
Also I've tried to create and ListCollectionView which was success, but the problem was that I could get the index of the ComboBox item. I prefer to work by an Id. For that reason I was looking for a new solution which I found at: http://zamjad.wordpress.com/2012/08/15/multi-columns-combo-box/
The problem with this example is that is not clear how the itemsource is being binded.
Edit:
To sum things up: I'm currently trying to bind a list(SearchPointsCollection) of objects(coSearchPoints) defined in a class (coImportReader).
namespace Import_Rates_Manager
{
public partial class MainWindow : Window
{
public coImportReader ir;
public coViewerControles vc;
public coSearchPoint sp;
public MainWindow()
{
InitializeComponent();
ir = new coImportReader();
vc = new coViewerControles();
sp = new coSearchPoint();
SearchRefPoint.DataContext = ir;
}
}
}
//in function....
SearchRefPoint.ItemsSource = ir.SearchPointCollection;
SearchRefPoint.DisplayMemberPath = "Name";
namespace Import_Rates_Manager
{
public class coImportReader
{
public List<coSearchPoint> SearchPointCollection = new List<coSearchPoint>();
}
}
namespace Import_Rates_Manager
{
public class coSearchPoint
{
public coSearchPoint()
{
string Name = "";
Guid Id = Guid.NewGuid();
IRange FoundCell = null;
}
}
}
This results in a filled combobox with no text
Here a simple example using the MVVM Pattern
XAML
<Window x:Class="Binding_a_List_to_a_ComboBox.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 HorizontalAlignment="Left"
VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<ComboBox Grid.Column="0" Grid.Row="0" ItemsSource="{Binding SearchPointCollection , UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding MySelectedIndex, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding MySelectedItem, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Id}" Grid.Row="0"/>
<TextBlock Text="{Binding Name}" Grid.Row="1"/>
<TextBlock Text="{Binding Otherstuff}" Grid.Row="2"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Content="Bind NOW" Grid.Column="0" Grid.Row="1" Click="Button_Click"/>
<TextBlock Text="{Binding MySelectedIndex, UpdateSourceTrigger=PropertyChanged}" Grid.Column="1" Grid.Row="0"/>
<Grid Grid.Column="1" Grid.Row="1"
DataContext="{Binding MySelectedItem, UpdateSourceTrigger=PropertyChanged}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Id}" Grid.Column="0"/>
<TextBlock Text="{Binding Name}" Grid.Column="1"/>
<TextBlock Text="{Binding SomeValue}" Grid.Column="2"/>
</Grid>
</Grid>
Code
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using Import_Rates_Manager;
namespace Binding_a_List_to_a_ComboBox
{
/// <summary>
/// Interaktionslogik für MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
DataContext = new coImportReader();
}
}
}
namespace Import_Rates_Manager
{
public class coImportReader : INotifyPropertyChanged
{
private List<coSearchPoint> myItemsSource;
private int mySelectedIndex;
private coSearchPoint mySelectedItem;
public List<coSearchPoint> SearchPointCollection
{
get { return myItemsSource; }
set
{
myItemsSource = value;
OnPropertyChanged("SearchPointCollection ");
}
}
public int MySelectedIndex
{
get { return mySelectedIndex; }
set
{
mySelectedIndex = value;
OnPropertyChanged("MySelectedIndex");
}
}
public coSearchPoint MySelectedItem
{
get { return mySelectedItem; }
set { mySelectedItem = value;
OnPropertyChanged("MySelectedItem");
}
}
#region cTor
public coImportReader()
{
myItemsSource = new List<coSearchPoint>();
myItemsSource.Add(new coSearchPoint { Name = "Name1" });
myItemsSource.Add(new coSearchPoint { Name = "Name2" });
myItemsSource.Add(new coSearchPoint { Name = "Name3" });
myItemsSource.Add(new coSearchPoint { Name = "Name4" });
myItemsSource.Add(new coSearchPoint { Name = "Name5" });
}
#endregion
#region INotifyPropertyChanged Member
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
public class coSearchPoint
{
public Guid Id { get; set; }
public String Name { get; set; }
public IRange FoundCell { get; set; }
public coSearchPoint()
{
Name = "";
Id = Guid.NewGuid();
FoundCell = null;
}
}
public interface IRange
{
string SomeValue { get; }
}
}
Here are 3 Classes:
MainWindow which set VM as his Datacontext
coImportReader the Class which presents your properties for your bindings
coSearchPoint which is just a Container for your information
IRange which is just an Interface
The Collection you are binding to needs to be a property of ir not a field.
Also try this :
public coImportReader ir { get; set; }
public <type of SearchPointCollection> irCollection { get { return ir != null ? ir.SearchPointCollection : null; } }
Bind to irCollection and see what errors you get if any.
The DisplayMemberPath should contain the property name of the elements in your collection. Assuming the elements in the SearchPointCollection are of the type SearchPoint and this class has a Property SearchPointName you should set DisplayMemberPath like this:
SearchRefPoint.DisplayMemberPath = "SearchPointName";
Edit:
In your code the class coSearchPoint has the Field Name defined in the Constructor. Name has to be a Property of the class, otherwise the Binding can't work.

Categories