Binding a Stack (that can be switched) to a ListBox - c#

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

Related

Cannot get correct WPF ComboBox behaviour (updating other controls only after FocusLost)

I have a Window where users can create a question. But they should also be able to pick an old one.
This Window has a ComboBox ('QuestionComboBox') with an array of Questions. These Questions have an array of Answers and a CorrectAnswer. The QuestionComboBox is Editable, so the user can start typing and a corresponding Question will appear. But a user must also be able to create a new question. But the handling of this is not relevant here (I think). I also removed the adding of Answers here to keep the example as small as possible.
But when the user starts typing, al the controls are updating there properties (as you can see in the RightAnswerComboBox in my example when you remove the UpdateSourceTrigger=LostFocus). This appears very nervous for the user.
Requirements:
The user should be able to create a new question or pick an old one
from the list.
And I want the RightAnswerComboBox only to be
updated when the user presses Enter or on FocusLost or when he/she
clicks a Question in the DropDown list to prevent a nervous user experience.
So I tried to add UpdateSourceTrigger=LostFocus to the SelectedItem. But then I get the behaviour that my ItemsSource changes as you can see in the picture. I also tried UpdateSourceTrigger=Explicit, but that I didn't get working properly at all (SelectionChanged was still called, even when the UpdateSource() was not triggered).
When the user types "Q1" (and TAB), then replaces the "1" in "3" (and TAB again) then the ItemsSource is changed to Q3, Q2, Q3 instead of Q1, Q2, Q3. This is not what I want. The ItemsSource should remain the same. If I change the TwoWay mode to OneWay, the RightAnswerComboBox (and my other controls) will not be updated. So I think I need TwoWay. This unwanted behaviour also disappears when I remove the UpdateSourceTrigger=LostFocus, but then I have the nervous behaviour back.
How can I make it possible so the QuestionComboBox fulfils my requirements above?
Here is the code:
Window1.xaml
<Window x:Class="WpfApp1.Window1"
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:WpfApp1"
mc:Ignorable="d"
Title="Window1" Height="450" Width="800">
<Window.DataContext>
<local:Window1ViewModel/>
</Window.DataContext>
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ComboBox x:Name="QuestionComboBox" Grid.Row="1"
Margin="10" VerticalAlignment="Center"
ItemsSource="{Binding Questions, Mode=OneWay}"
Text="{Binding Question.Text, UpdateSourceTrigger=LostFocus}"
SelectedItem="{Binding DataContext.Question, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, UpdateSourceTrigger=LostFocus, Mode=TwoWay}"
DisplayMemberPath="Text" IsEditable="True" SelectionChanged="QuestionComboBox_SelectionChanged" />
<ComboBox x:Name="RightAnswerComboBox" DisplayMemberPath="Text"
Grid.Row="2" VerticalAlignment="Center" ItemsSource="{Binding Question.Answers}" SelectedItem="{Binding Question.RightAnswer}" />
</Grid>
</Grid>
</Window>
Window1ViewModel.cs
class Window1ViewModel
{
public ObservableCollection<Question> Questions { get; set; }
public Question Question { get; set; }
public Window1ViewModel()
{
var answers = new[]
{
new Answer(1, "A11"), new Answer(2, "A12"), new Answer(3, "A13"),
new Answer(1, "A21"), new Answer(2, "A22"), new Answer(3, "A23"),
new Answer(1, "A31"), new Answer(2, "A32"), new Answer(3, "A33"),
};
Questions = new ObservableCollection<Question>
{
new Question {Text="Q1", Answers = new[]
{
answers[0],
answers[1],
answers[2],
},
RightAnswer = answers[0]
},
new Question {Text="Q2", Answers = new[]
{
answers[3],
answers[4],
answers[5],
},
RightAnswer = answers[4]
},
new Question {Text="Q3", Answers = new[]
{
answers[6],
answers[7],
answers[8],
},
RightAnswer = answers[8]
},
};
}
}
public class Question
{
public Answer[] Answers { get; set; }
public string Text { get; set; }
public Answer RightAnswer { get; set; }
}
public class Answer : INotifyPropertyChanged
{
private string _text;
public Answer(int id, string text)
{
Id = id;
Text = text;
}
public int Id { get; set; }
public string Text
{
get => _text;
set
{
if (value == _text)
return;
_text = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You will get the behaviour you are looking for by not binding to the SelectedItem but to the SelectedValue. While using the item is advisable in most cases, this is one where value might be the better choise as the item that is to be selected may not yet exist.
Here is some minimal code:
<Window x:Class="CSharpSandbox.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:CSharpSandbox"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="400">
<Window.DataContext>
<local:MainVM/>
</Window.DataContext>
<Grid>
<StackPanel>
<ComboBox IsEditable="True"
ItemsSource="{Binding AvaliableQuestions}"
SelectedValue="{Binding SelectedQuestion}"
Text="{Binding Selected, UpdateSourceTrigger=LostFocus}"
DisplayMemberPath="Name"/>
<TextBlock Name="SomeOtherElementDisplayingAQuestionProperty"
Text="{Binding SelectedQuestion.Name}"/>
<TextBox Name="MakeItLoseFocusTextBox"/>
</StackPanel>
</Grid>
</Window>
public class MainVM : INotifyPropertyChanged
{
private Question _selectedQ;
public Question SelectedQuestion
{
get { return _selectedQ; }
set
{
if (_selectedQ != value)
{
_selectedQ = value;
OnPropertyChanged();
}
}
}
private string _selectedN;
public string SelectedName
{
get { return _selectedN; }
set
{
if (_selectedN != value)
{
_selectedN = value;
OnSelectedNameChanged(value);
OnPropertyChanged();
}
}
}
public ObservableCollection<Question> AvaliableQuestions { get; set; }
public MainVM ()
{
AvaliableQuestions = new ObservableCollection<Question>();
AvaliableQuestions.Add(new Question("Q1"));
AvaliableQuestions.Add(new Question("Q2"));
AvaliableQuestions.Add(new Question("Q3"));
}
private void OnSelectedNameChanged(string name)
{
Question tempQ = AvaliableQuestions.First(q => q.Name == name);
if (tempQ == null)
{
tempQ = new Question(name);
AvaliableQuestions.Add(tempQ);
}
SelectedQuestion = tempQ;
}
}
public class Question
{
public string Name { get; private set; }
public Question(string name)
{
Name = name;
}
}

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

Binding to a UserControl in WPF using C#

Preface
The control I am giving as an example is an sample work for a larger project. I have already had some help from the community on Stackoverflow ironing out some of the finer points of bindings within the control. The surprise has been that I am having an issue binding in the control's hosting form.
I have read and researched around DependencyProperty for a lot of hours. I was not a WPF developer at the start of the year but I am now covering the role because of a death in the business, and I accept this is a big hill to climb.
The question is what is missing here in my:
The hosting form's XAML code
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AControl="clr-namespace:AControl;assembly=AControl" x:Class="DependencySampler.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Grid>
<AControl:UserControl1 x:Name="cboBob" HorizontalAlignment="Left" Margin="100,118,0,0" VerticalAlignment="Top" Width="200" Height="29" SelectedColor="{Binding Path=BeSelected, Mode=OneWayToSource}"/>
</Grid>
The code behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new viewModelBinding();
BeSelected = new modelMain("Yellow", "#FFFFE0");
}
public modelMain BeSelected
{
get { return ((viewModelBinding)DataContext).Selected; }
set { ((viewModelBinding)DataContext).Selected = value; }
}
}
The ViewModel
public class viewModelBinding :ViewModelBase
{
modelMain sel = new modelMain("Red", "#FF0000");
public modelMain Selected
{
get { return sel; }
set { SetProperty(ref this.sel, value, "Selected"); }
}
}
The next section is the control itself.
The Model
public class modelMain:ViewModelBase
{
public modelMain(string colName, string hexval)
{
ColorName = colName;
HexValue = hexval;
}
string colorName;
public string ColorName
{
get { return colorName; }
set { SetProperty(ref this.colorName, value, "ColorName"); }
}
string hexValue;
public string HexValue
{
get { return hexValue; }
set { SetProperty(ref this.hexValue, value, "HexValue"); }
}
}
The ViewModel
public class viewModelMain:ViewModelBase
{
ObservableCollection<modelMain> val = new ObservableCollection<modelMain>();
public ObservableCollection<modelMain> ColorsList
{
get { return val; }
set { SetProperty(ref this.val, value, "Colors"); }
}
modelMain selectedColor;
public modelMain SelectedColour
{
get{return selectedColor;}
set { SetProperty(ref this.selectedColor, value, "SelectedColour"); }
}
public void SetCurrentColor(modelMain col)
{
SelectedColour = this.val.Where(x => x.ColorName == col.ColorName).FirstOrDefault();
}
public viewModelMain()
{
val.Add(new modelMain("Red", "#FF0000"));
val.Add(new modelMain("Blue", "#0000FF"));
val.Add(new modelMain("Green", "#008000"));
val.Add(new modelMain("Yellow", "#FFFFE0"));
SelectedColour = new modelMain("Blue", "#0000FF");
}
}
The UserControl XAML
<UserControl x:Class="AControl.UserControl1"
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="32" d:DesignWidth="190">
<Grid>
<ComboBox x:Name="cboValue"
SelectionChanged="cboValue_SelectionChanged"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{Binding ColorList, RelativeSource={RelativeSource AncestorType=UserControl}}"
SelectedValue="{Binding SelectedColor, RelativeSource={RelativeSource AncestorType=UserControl}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width="10"
Height="10"
Margin="5"
Background="{Binding ColorName}"/>
<TextBlock Width="35"
Height="15"
Margin="5"
Text="{Binding ColorName}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
The UserControl Code behind
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
ObservableCollection<modelMain> colorList = new viewModelMain().ColorsList;
public ObservableCollection<modelMain> ColorList
{
get { return colorList; }
set { colorList = value; }
}
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(
"SelectedColor",
typeof(modelMain),
typeof(UserControl1),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(OnSelectedColorChanged),
new CoerceValueCallback(CoerceSelectedColorCallback)));
private static void OnSelectedColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UserControl1 uc = (UserControl1)d;
uc.SelectedColor = (modelMain)e.NewValue;
}
private static object CoerceSelectedColorCallback(DependencyObject d, object value)
{
return (modelMain)value;
}
public modelMain SelectedColor
{
get { return (modelMain)GetValue(SelectedColorProperty); }
set { SetValue(SelectedColorProperty, value); }
}
private void cboValue_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var dat = sender as ComboBox;
SelectedColor = (modelMain)dat.SelectedValue;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
//var dat = sender as ComboBox;
////SelectedColor = (modelMain)dat.SelectedValue;
//SelectedColor = (modelMain)this.SelectedColor;
}
}
Please note that in the code behind there is unused code but within the sample I have used then for placing break points
I understand that no DataContext should exist in the UserControl because it precludes one in the hosting form.
The Question
I was expecting the this line would be sufficient in the hosting form.
<AControl:UserControl1 x:Name="cboBob" HorizontalAlignment="Left" Margin="100,118,0,0" VerticalAlignment="Top" Width="200" Height="29" SelectedColor="{Binding Path=BeSelected, Mode=OneWayToSource}"/>
But it does not seem to do what I expected. I can see the BeSelected be initialised and it is holding a value but when the form loads I am expecting the colour yellow to enter the UserControl's and set DependencyProperty SelectedColor. This is not happening why and how can I get it to happen?
To get you example working, do the following (for the most part, implement what the commenters said):
The hosting form's XAML code
<AControl:UserControl1 x:Name="cboBob" HorizontalAlignment="Left" Margin="100,118,0,0" VerticalAlignment="Top" Width="200" Height="29" SelectedColor="{Binding Path=BeSelected, RelativeSource={RelativeSource AncestorType=Window}}}"/>
The Mode doesn't really matter since MainWindow doesn't implement INPC nor does it ever know when ((viewModelBinding)DataContext).Selected (and therefor, BeSelected) is changed. Actually, like Joe stated, OneWayToSource doesn't work... RelativeSource was needed because BeSelected is a property of the MainWindow - not MainWindow's DataContext.
modelMain
modelMain needs to implement IEquatable (like Janne commented). Why? Because BeSelected = new modelMain(...) creates a new modelMain which is not one of the items in the ComboBox's ItemsSource (ColorList). The new object may have the same property values as one of the items but that doesn't make them equal (different objects = different address in memory). IEquatable gives you the opportunity to override that.
public class modelMain : ViewModelBase, IEquatable<modelMain>
{
...
public bool Equals(modelMain other)
{
return (HexValue == other.HexValue);
}
}
viewModelMain's ColorList's setter is calling SetProperty with property name "Colors" when it should be "ColorsList". It's not being used so it doesn't stop your example from working but it's still wrong.

ListView not updated after adding new items to List

I have a ListView bounded to a List of a class I created. When doing an operating, it was supposed to add/remove items from the list, but my ListView wasn't updated even though I used INotifyPropertyChanged.
If I use ObservableCollection, it works but I need to have the list sorted, and ObservableCollection doesn't do sorting for WPF4.0 :(
Any way I can make the List binding work? Why didn't it work even though I used INotifyPropertyChanged?
XAML:
<ListView BorderThickness="0" ItemsSource="{Binding SelectedValues, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" Padding="5">
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource myHeaderStyle}">
<GridViewColumn DisplayMemberBinding="{Binding Value}"></GridViewColumn>
VM:
private List<CheckBoxItem> _selectedValues = new List<CheckBoxItem>();
public List<CheckBoxItem> SelectedValues
{
get { return _selectedValues; }
set
{
_selectedValues = value;
OnPropertyChanged();
}
}
private void UnselectValueCommandExecute(CheckBoxItem value)
{
value.IsSelected = false;
SelectedValues.Remove(value);
//OnPropertyChanged("SelectedValues");
OnPropertyChanged("IsAllFilteredValuesSelected");
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
The CheckBoxItem class contains 2 properties, Value and IsChecked, which I don't think is relevant here.
So basically, I have a button which uses the UnselectValueCommandExecute to remove items from the list, and I should see the list updated in the UI, but I'm not.
When I debug, I can see the SelectedValues list updated, but not my UI.
You need a CollectionViewSource in your UI.
The XAML:
<Window x:Class="WavTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<CollectionViewSource Source="{Binding TestSource}" x:Key="cvs">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Order"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
<ListView ItemsSource="{Binding Source={StaticResource cvs}}" DisplayMemberPath="Description"/>
</Window>
The code behind:
namespace WavTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
this.DataContext = vm;
vm.TestSource.Add(new TestItem { Description="Zero", Order=0 });
}
}
public class ViewModel
{
public ObservableCollection<TestItem> TestSource { get; set; }
public ViewModel()
{
TestSource = new ObservableCollection<TestItem>();
TestSource.Add(new TestItem { Description = "Second", Order = 2 });
TestSource.Add(new TestItem { Description = "Third", Order = 3 });
TestSource.Add(new TestItem { Description = "First", Order = 1 });
}
}
public class TestItem
{
public int Order { get; set; }
public string Description { get; set; }
}
}
Explanation:
The ObservableCollection raises the PropertyChanged event as you expect, but you cannot sort it.
So, you need the CollectionView to sort it and bind the sorted collection to you ListView/ListBox.
As you can see, adding an element after the DataContext initialization affects the UI sorting the last added item ("Zero") correctly.
You need to use ObservableCollection because this raises a collection changed event which your wpf ListView will pick up on.
How about doing
Public ObservableCollection<object> MyList
{
get
{
return new ObservableCollection<object>(MySortedList);
}
}
and then whenever you change your sorted list raise a property changed event for MyList.
This obviously depends how you would like to sort your list as it might be possible to sort the ObservableCollection your question needs more info.

How can i know if a ListBoxItem is the last item inside a Wpf's ListBox?

How can i know if a ListBoxItem is the last item of the collection (in the ItemContainerStyle or in the ItemContainer's template) inside a Wpf's ListBox?
That question is because I need to know if an item is the last item to show it in other way. For example: suppose i want to show items separated by semi-colons but the last one: a;b;c
This is easy to do in html and ccs, using ccs selector. But, how can i do this in Wpf?
As it seems to be rather difficult to implement an "Index" attached property to ListBoxItem to do the job right, I believe the easier way to accomplish that would be in MVVM.
You can add the logic necessary (a "IsLast" property, etc) to the entity type of the list and let the ViewModel deal with this, updating it when the collection is modified or replaced.
EDIT
After some attempts, I managed to implement indexing of ListBoxItems (and consequently checking for last) using a mix of attached properties and inheriting ListBox. Check it out:
public class IndexedListBox : System.Windows.Controls.ListBox
{
public static int GetIndex(DependencyObject obj)
{
return (int)obj.GetValue(IndexProperty);
}
public static void SetIndex(DependencyObject obj, int value)
{
obj.SetValue(IndexProperty, value);
}
/// <summary>
/// Keeps track of the index of a ListBoxItem
/// </summary>
public static readonly DependencyProperty IndexProperty =
DependencyProperty.RegisterAttached("Index", typeof(int), typeof(IndexedListBox), new UIPropertyMetadata(0));
public static bool GetIsLast(DependencyObject obj)
{
return (bool)obj.GetValue(IsLastProperty);
}
public static void SetIsLast(DependencyObject obj, bool value)
{
obj.SetValue(IsLastProperty, value);
}
/// <summary>
/// Informs if a ListBoxItem is the last in the collection.
/// </summary>
public static readonly DependencyProperty IsLastProperty =
DependencyProperty.RegisterAttached("IsLast", typeof(bool), typeof(IndexedListBox), new UIPropertyMetadata(false));
protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
{
// We capture the ItemsSourceChanged to check if the new one is modifiable, so we can react to its changes.
var oldSource = oldValue as INotifyCollectionChanged;
if(oldSource != null)
oldSource.CollectionChanged -= ItemsSource_CollectionChanged;
var newSource = newValue as INotifyCollectionChanged;
if (newSource != null)
newSource.CollectionChanged += ItemsSource_CollectionChanged;
base.OnItemsSourceChanged(oldValue, newValue);
}
void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.ReindexItems();
}
protected override void PrepareContainerForItemOverride(System.Windows.DependencyObject element, object item)
{
// We set the index and other related properties when generating a ItemContainer
var index = this.Items.IndexOf(item);
SetIsLast(element, index == this.Items.Count - 1);
SetIndex(element, index);
base.PrepareContainerForItemOverride(element, item);
}
private void ReindexItems()
{
// If the collection is modified, it may be necessary to reindex all ListBoxItems.
foreach (var item in this.Items)
{
var itemContainer = this.ItemContainerGenerator.ContainerFromItem(item);
if (itemContainer == null) continue;
int index = this.Items.IndexOf(item);
SetIsLast(itemContainer, index == this.Items.Count - 1);
SetIndex(itemContainer, index);
}
}
}
To test it, we setup a simple ViewModel and an Item class:
public class ViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
private ObservableCollection<Item> items;
public ObservableCollection<Item> Items
{
get { return this.items; }
set
{
if (this.items != value)
{
this.items = value;
this.OnPropertyChanged("Items");
}
}
}
public ViewModel()
{
this.InitItems(20);
}
public void InitItems(int count)
{
this.Items = new ObservableCollection<Item>();
for (int i = 0; i < count; i++)
this.Items.Add(new Item() { MyProperty = "Element" + i });
}
}
public class Item
{
public string MyProperty { get; set; }
public override string ToString()
{
return this.MyProperty;
}
}
The view:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication3"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="WpfApplication3.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="DataTemplate">
<Border x:Name="border">
<StackPanel Orientation="Horizontal">
<TextBlock TextWrapping="Wrap" Text="{Binding (local:IndexedListBox.Index), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Margin="0,0,8,0"/>
<TextBlock TextWrapping="Wrap" Text="{Binding (local:IndexedListBox.IsLast), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Margin="0,0,8,0"/>
<ContentPresenter Content="{Binding}"/>
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding (local:IndexedListBox.IsLast), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Value="True">
<Setter Property="Background" TargetName="border" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="0.949*"/>
</Grid.RowDefinitions>
<local:IndexedListBox ItemsSource="{Binding Items}" Grid.Row="1" ItemTemplate="{DynamicResource DataTemplate}"/>
<Button Content="Button" HorizontalAlignment="Left" Width="75" d:LayoutOverrides="Height" Margin="8" Click="Button_Click"/>
<Button Content="Button" HorizontalAlignment="Left" Width="75" Margin="110,8,0,8" Click="Button_Click_1" d:LayoutOverrides="Height"/>
<Button Content="Button" Margin="242,8,192,8" Click="Button_Click_2" d:LayoutOverrides="Height"/>
</Grid>
</Window>
In the view's code behind I put some logic to test the behavior of the solution when updating the collection:
public partial class MainWindow : Window
{
public ViewModel ViewModel { get { return this.DataContext as ViewModel; } }
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.ViewModel.Items.Insert( 5, new Item() { MyProperty= "NewElement" });
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
this.ViewModel.Items.RemoveAt(5);
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
this.ViewModel.InitItems(new Random().Next(10,30));
}
}
This solution can handle static lists and also ObservableCollections and adding, removing, inserting items to it. Hope you find it useful.
EDIT
Tested it with CollectionViews and it works just fine.
In the first test, I changed Sort/GroupDescriptions in the ListBox.Items. When one of them was changed, the ListBox recreates the containeirs, and then PrepareContainerForItemOverride hits. As it looks for the right index in the ListBox.Items itself, the order is updated correctly.
In the second I made the Items property in the ViewModel a ListCollectionView. In this case, when the descriptions were changed, the CollectionChanged was raised and the ListBox reacted as expected.

Categories