Reputation: 185
I have a ComboBox control in my MainWindow.xaml that is bound to a List<Job>
where Job
is a class that has multiple properties and implements INotifyPropertyChange
and IComparable
(I also tried removing IComparable
implementation). The MainWindow class also implements INotifyPropertyChange
. When I load this list from a database, the bound ComboBox
control is populated correctly. The ComboBox
control is defined in xaml as
<ComboBox x:Name="JobsComboBox" ItemsSource = "{Binding Path=AllJobs, Mode=TwoWay}" IsSynchronizedWithCurrentItem="True"
ScrollViewer.VerticalScrollBarVisibility="Auto" MaxHeight="400"
MaxWidth="400" HorizontalContentAlignment="Center" />
I have a separate control that allows the user to sort by a particular property of Job
. When the user selects a different sort property, I use a delegate
to sort my list and notify the change like so:
public void SortJobsListByProperty(string? propertyName)
{
if (AllJobs != null) AllJobs.Sort(delegate(Job x, Job y)
{
int result = 0;
switch (propertyName)
{
case "Quantity":
result = x.Quantity.CompareTo(y.Quantity);
break;
case "Due Date":
result = x.DueDate.CompareTo(y.DueDate);
break;
case "Siemens PN":
result = x.Assembly.SiemensPN.CompareTo(y.Assembly.SiemensPN);
break;
case "Customer PN":
result = x.Assembly.CustomerPN.CompareTo(y.Assembly.CustomerPN);
break;
default:
result = x.JobNumber.CompareTo(y.JobNumber);
break;
}
return result;
});
NotifyPropertyChange("AllJobs");
}
Here's where it gets weird: If the ComboBox
has keyboard focus, I can navigate through the items with the up/down arrow keys, and the selected item is in the correct sorted order. However, if I click the drop-down arrow on the ComboBox
, the displayed list is NOT in the correct order, but shows the order as how the list was originally loaded. It's as if the selected item index follows the sorting, but the displayed drop-down is not updated.
I have searched high and low for other threads with similar problems to no avail. The closest solution I found says that anObservableCollection
should be used instead of List<Class>
if the collection needs to be sorted into a ComboBox
. But I can't figure out how to sort an ObservableCollection
and feel that the order in the drop-down should match the arrow key navigation. The control is obviously seeing the change to the list or the arrow keys would not navigate in correct order as I understand. I am new to data-binding and property change notifications so I apologize if I'm missing something obvious. Also apologize if poorly formatted or unclear, I can try to clarify. Thanks in advance for your help!
Edit: I also notice that if I use the drop-down to select a ListItem, the current item is actually the correct item, just the string in the combo box selection is incorrect. E.g. If I navigate to the 3rd item in the ListBox either by arrow key or drop-down selection, the current item is actually the 3rd item in the current sort order, but it's properties do not match the ToString() method of that item's field. The 3rd JobNumber is 123456 and the third item in the drop-down is 123456, but the string displayed for that selection contains some other JobNumber like 666666 based on the original sorting. Seems like the ComboBox actually makes the correct selection, but its string doesn't match.
Upvotes: 0
Views: 213
Reputation: 7908
Despite the fact that you have found a solution that suits you, I still provide additional explanations and examples for implementing sorting.
using System;
using System.Globalization;
namespace Core2022.SO.JSteh.FixedList
{
public class Job
{
public int JobNumber { get; }
public string SiemensPN { get; }
public string Revision { get; }
public string Customer { get; }
public string CustomerPN { get; }
public int Quantity { get; }
public DateTime DueDate { get; }
public string Description { get; }
public Job(int jobNumber, string siemensPN, string revision, string customer, string customerPN, int quantity, DateTime dueDate, string description)
{
JobNumber = jobNumber;
SiemensPN = siemensPN ?? string.Empty;
Revision = revision ?? string.Empty;
Customer = customer ?? string.Empty;
CustomerPN = customerPN ?? string.Empty;
Quantity = quantity;
DueDate = dueDate;
Description = description ?? string.Empty;
}
public static Job Parse(string line)
{
var split = line.Split(';', StringSplitOptions.TrimEntries);
return new Job(int.Parse(split[0]), split[1], split[2], split[3], split[4], int.Parse(split[5]), DateTime.Parse(split[6], CultureInfo.InvariantCulture), split[7]);
}
public override string ToString() => $"{JobNumber}; {SiemensPN} ({CustomerPN})";
}
}
using Simplified;
using System.Collections.Generic;
using System.Linq;
namespace Core2022.SO.JSteh.FixedList
{
public class JobsViewModel : BaseInpc
{
private readonly List<Job> originalJobsList = new List<Job>()
{
Job.Parse("211154; R-PC010004578; A1X; TECONNECT; 1-2293506-2; 12610; 9/30/2022; RvA3 [66] DANA DIFF SENSOR ROHS"),
Job.Parse("331154; A-PC010004578; A1X; TECONNECT; 1-2293506-1; 12600; 1/30/2022; [66] DANA DIFF SENSOR ROHS"),
Job.Parse("991154; Z-PC010004578; A1X; TECONNECT; 1-2293506-0; 13600; 9/25/2022; RvA3 [66] DIFF SENSOR ROHS"),
Job.Parse("551154; R-PC010009999; A1X; TECONNECT; 1-3335806-2; 82600; 9/30/2019; RvA3 [66] DANA SENSOR ROHS"),
Job.Parse("881154; R-PC009004578; A1X; TECONNECT; 1-8893506-2; 10999; 5/25/2019; RvA3 [66] DANA DIFF ROHS"),
Job.Parse("011154; R-PC000000999; A1X; TECONNECT; 1-1113506-2; 00050; 1/01/2011; RvA3 DANA DIFF SENSOR ROHS"),
};
private IEnumerable<Job> _allJobs = Enumerable.Empty<Job>();
private Job? _selectedJob;
public IEnumerable<Job> AllJobs { get => _allJobs; set => Set(ref _allJobs, value); }
public Job? SelectedJob { get => _selectedJob; set => Set(ref _selectedJob, value); }
public JobsViewModel()
{
SortJobsListByProperty(string.Empty);
SortJobsCommand = new RelayCommand<string>(SortJobsListByProperty);
}
public RelayCommand SortJobsCommand { get; }
private void SortJobsListByProperty(string propertyName)
{
AllJobs = propertyName switch
{
"Quantity" => originalJobsList.OrderBy(job => job.Quantity),
"Due Date" => originalJobsList.OrderBy(job => job.DueDate),
"Siemens PN" => originalJobsList.OrderBy(job => job.SiemensPN),
"Customer PN" => originalJobsList.OrderBy(job => job.Customer),
_ => originalJobsList.Select(job => job)
};
}
public IEnumerable<string> SortNames { get; }
= "No Sorting;Quantity;Due Date;Siemens PN;Customer PN"
.Split(';')
.Select(line => line);
}
}
<Window x:Class="Core2022.SO.JSteh.FixedList.JobsWindow"
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:Core2022.SO.JSteh.FixedList"
mc:Ignorable="d"
Title="JobsWindow" Height="450" Width="800"
DataContext="{DynamicResource vm}">
<Window.Resources>
<local:JobsViewModel x:Key="vm"/>
</Window.Resources>
<UniformGrid Columns="2">
<ListBox ItemsSource="{Binding AllJobs}"
SelectedItem="{Binding SelectedJob}"
SelectedIndex="1"/>
<StackPanel DataContext="{Binding SelectedJob}">
<TextBlock Text="{Binding JobNumber, StringFormat={}Job Number: {0}}"/>
<TextBlock Text="{Binding SiemensPN, StringFormat={}Siemens Part Num: {0}}"/>
<TextBlock Text="{Binding Revision, StringFormat={}Revision: {0}}"/>
<TextBlock Text="{Binding Customer, StringFormat={}Customer: {0}}"/>
<TextBlock Text="{Binding CustomerPN, StringFormat={}Customer Part Num: {0}}"/>
<TextBlock Text="{Binding Quantity, StringFormat={}Quantity: {0}}"/>
<TextBlock Text="{Binding DueDate, StringFormat={}Due Date: {0}}"/>
<TextBlock Text="{Binding Description, StringFormat={}Description: {0}}"/>
</StackPanel>
<ListBox x:Name="listBox"
ItemsSource="{Binding SortNames}"
SelectedIndex="0"/>
<Button Content="Refresh"
Command="{Binding SortJobsCommand}"
CommandParameter="{Binding SelectedItem, ElementName=listBox}"/>
</UniformGrid>
</Window>
Implementation of the BaseInpc and RelayCommand classes.
using Simplified;
using System;
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
namespace Core2022.SO.JSteh.DynamicList
{
public class Job : BaseInpc
{
private int _jobNumber;
private string _siemensPN = string.Empty;
private string _revision = string.Empty;
private string _customer = string.Empty;
private string _customerPN = string.Empty;
private int _quantity;
private DateTime _dueDate;
private string _description = string.Empty;
public int JobNumber { get => _jobNumber; set => Set(ref _jobNumber, value); }
public string SiemensPN { get => _siemensPN; set => Set(ref _siemensPN, value ?? string.Empty); }
public string Revision { get => _revision; set => Set(ref _revision, value ?? string.Empty); }
public string Customer { get => _customer; set => Set(ref _customer, value ?? string.Empty); }
public string CustomerPN { get => _customerPN; set => Set(ref _customerPN, value ?? string.Empty); }
public int Quantity { get => _quantity; set => Set(ref _quantity, value); }
public DateTime DueDate { get => _dueDate; set => Set(ref _dueDate, value); }
public string Description { get => _description; set => Set(ref _description, value ?? string.Empty); }
public Job()
{ }
public Job(int jobNumber, string siemensPN, string revision, string customer, string customerPN, int quantity, DateTime dueDate, string description)
{
JobNumber = jobNumber;
SiemensPN = siemensPN ?? string.Empty;
Revision = revision ?? string.Empty;
Customer = customer ?? string.Empty;
CustomerPN = customerPN ?? string.Empty;
Quantity = quantity;
DueDate = dueDate;
Description = description ?? string.Empty;
}
public static Job Parse(string line)
{
var split = line.Split(';', StringSplitOptions.TrimEntries);
return new Job(int.Parse(split[0]), split[1], split[2], split[3], split[4], int.Parse(split[5]), DateTime.Parse(split[6], CultureInfo.InvariantCulture), split[7]);
}
public override string ToString() => $"{JobNumber}; {SiemensPN} ({CustomerPN})";
}
}
using Simplified;
using System.Collections.ObjectModel;
namespace Core2022.SO.JSteh.DynamicList
{
public class JobsViewModel : BaseInpc
{
private Job? _selectedJob;
public ObservableCollection<Job> AllJobs { get; } = new ObservableCollection<Job>()
{
Job.Parse("211154; R-PC010004578; A1X; TECONNECT; 1-2293506-2; 12610; 9/30/2022; RvA3 [66] DANA DIFF SENSOR ROHS"),
Job.Parse("331154; A-PC010004578; A1X; TECONNECT; 1-2293506-1; 12600; 1/30/2022; [66] DANA DIFF SENSOR ROHS"),
Job.Parse("991154; Z-PC010004578; A1X; TECONNECT; 1-2293506-0; 13600; 9/25/2022; RvA3 [66] DIFF SENSOR ROHS"),
Job.Parse("551154; R-PC010009999; A1X; TECONNECT; 1-3335806-2; 82600; 9/30/2019; RvA3 [66] DANA SENSOR ROHS"),
Job.Parse("881154; R-PC009004578; A1X; TECONNECT; 1-8893506-2; 10999; 5/25/2019; RvA3 [66] DANA DIFF ROHS"),
Job.Parse("011154; R-PC000000999; A1X; TECONNECT; 1-1113506-2; 00050; 1/01/2011; RvA3 DANA DIFF SENSOR ROHS"),
};
public Job? SelectedJob { get => _selectedJob; set => Set(ref _selectedJob, value); }
}
}
<Window x:Class="Core2022.SO.JSteh.DynamicList.JobsWindow"
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:Core2022.SO.JSteh.DynamicList"
mc:Ignorable="d"
Title="JobsWindow" Height="450" Width="800"
DataContext="{DynamicResource vm}">
<Window.Resources>
<local:JobsViewModel x:Key="vm"/>
<local:Job x:Key="newJob"/>
</Window.Resources>
<UniformGrid Columns="2">
<ListBox x:Name="jobsList"
ItemsSource="{Binding AllJobs}"
SelectedItem="{Binding SelectedJob}"
SelectedIndex="1"/>
<StackPanel DataContext="{Binding SelectedJob}">
<UniformGrid Columns="2">
<TextBlock Text="Number:"/>
<TextBox Text="{Binding JobNumber, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Siemens Part Num:"/>
<TextBox Text="{Binding SiemensPN, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Revision:"/>
<TextBox Text="{Binding Revision, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Customer:"/>
<TextBox Text="{Binding Customer, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Customer Part Num: "/>
<TextBox Text="{Binding CustomerPN, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Quantity:"/>
<TextBox Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Due Date:"/>
<TextBox Text="{Binding DueDate, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Description:}"/>
<TextBox Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}"/>
</UniformGrid>
</StackPanel>
<StackPanel>
<ListBox x:Name="listBox"
ItemsSource="{x:Static local:ViewHelper.SortNames}"
SelectedIndex="0"/>
<Button Content="Change Sort" Margin="5" Padding="15 5"
Click="OnSortJobsClicker"/>
</StackPanel>
<StackPanel>
<UniformGrid Columns="2" DataContext="{DynamicResource newJob}">
<TextBlock Text="Number:"/>
<TextBox Text="{Binding JobNumber, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Siemens Part Num:"/>
<TextBox Text="{Binding SiemensPN, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Revision:"/>
<TextBox Text="{Binding Revision, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Customer:"/>
<TextBox Text="{Binding Customer, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Customer Part Num: "/>
<TextBox Text="{Binding CustomerPN, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Quantity:"/>
<TextBox Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Due Date:"/>
<TextBox Text="{Binding DueDate, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="Description:}"/>
<TextBox Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}"/>
</UniformGrid>
<Button Content="Add new Job" Margin="5" Padding="15 5"
Click="OnAddJobsClicker"/>
</StackPanel>
</UniformGrid>
</Window>
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace Core2022.SO.JSteh.DynamicList
{
public partial class JobsWindow : Window
{
public JobsWindow()
{
InitializeComponent();
jobsList.Items.IsLiveSorting = true;
}
private void OnSortJobsClicker(object sender, RoutedEventArgs e)
{
jobsList.Items.SortDescriptions.Clear();
jobsList.Items.LiveSortingProperties.Clear();
string? sortItem = listBox.SelectedItem as string;
string? propertyName = sortItem switch
{
"Quantity" => "Quantity",
"Due Date" => "DueDate",
"Siemens PN" => "SiemensPN",
"Customer PN" => "Customer",
_ => null
};
if (propertyName is not null)
{
jobsList.Items.SortDescriptions.Add(new SortDescription() { PropertyName = propertyName });
jobsList.Items.LiveSortingProperties.Add(propertyName);
}
}
private void OnAddJobsClicker(object sender, RoutedEventArgs e)
{
Job newJob =(Job) Resources["newJob"];
JobsViewModel vm = (JobsViewModel) Resources["vm"];
vm.AllJobs.Add(newJob);
Resources["newJob"] = new Job();
}
}
public static class ViewHelper
{
public static IEnumerable<string> SortNames { get; }
= "No Sorting;Quantity;Due Date;Siemens PN;Customer PN"
.Split(';')
.Select(line => line);
}
}
Upvotes: 1
Reputation: 6724
Slightly more in-depth answer/explanation:
The code NotifyPropertyChange("AllJobs")
is notifying all listeners that "the value of this property has changed". But, technically, it hasn't. The AllJobs
property is still holding the same List<Job>
instance. Something inside that List<Job>
has changed (the order of the items), but it's still the same object (at no point was there ever a new List<Job>()
). So the data binding framework gets the NotifyPropertyChange
call, examines the value of AllJobs
, realizes the List<Job>
that is there now is the same List<Job>
it already has, and then leaves it at that assuming everything is already up to date. It assumes nothing inside that object has changed because nothing is telling it otherwise.
ObservableCollection<T>
implements INotifyCollectionChanged
, which is how you tell the binding system "one or more of the elements in my list has changed, please update it".
This answer actually has most of what you need. It gives you the "proper solution" of CollectionViewSource
, but also does show you how you could sort an ObservableCollection<T>
if you really want to (by creating a new ObservableCollection<T>
and using OrderBy
from LINQ).
Upvotes: 1
Reputation: 185
Ok, after messing around with a lot of hopeful fixes, I finally found what works. Apparently the ComboBox
items do not update when the sort order of the bound list changes. Instead, the ComboBox
's underlying Items
property must be refreshed. So I simply added JobsComboBox.Items.Refresh()
to the bottom of my SortJobsListByProperty(string)
method, after sorting the bound List<Job>
, and everything is working like it should. Strings in the ComboBox
are updated and match with their synchronized current item.
Hope this helps someone wrestling with this problem in the future.
Upvotes: 0