Sort ObservableCollection - what is the best approach? - wpf

I have a ObservableCollection , where MyData is a class with 4 properties i.e. int id, string name, bool IsSelected, string IsVisible.
This ObservableCollection is binded to a combobox with checkboxes(Cities data for example). Now, when the user checks the checkboxes then next time when he opens the drop down - all selections should come on top in ascending order by name.
I have also implemented auto complete when the user types in 3 chars in the combobox, the drop down will open showing all the selections first, then then all the items starting from the 3 chars type in by the user.
I have researched and implemented the following code and it is working fine, but i want to know whether this is the best approach or can i implement this in a better manner, code is :
IEnumerable<MyData> sort;
ObservableCollection<MyData> tempSortedCities = new ObservableCollection<MyData>();
sort = City.OrderByDescending(item => item.IsSelected).ThenBy(item => item.Name.ToUpper()) ;
// City is my observablecollection<MyData> property in my Model binded to combobox in UI
foreach (var item in sort)
tempSortedCities.Add(item);
City.Clear(); // City is my observablecollection<MyData> property in my Model
City = tempSortedCities;
tempSortedCities = null;
sort = null;
Thanks in advance for your time !

ICollectionView seems to be a perfect fit for this. It was designed specifically for sorting, filtering and grouping of a collection without modifying the original collection.
You can get an instance of ICollectionView for your collection using the following code:
var sortedCities = CollectionViewSource.GetDefaultView(City);
Then you can setup sorting by adding instances of SortDescription type to the ICollectionView.SortDescriptions collection:
sortedCities.SortDescriptions.Add(new SortDescription("IsSelected", ListSortDirection.Descending));
sortedCities.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
Then you can bind your ComboBox directly to the collection view (instead of City collection) and it will display already sorted data.

Related

ListView CollectionView sort lowest level

I've a question about how to sort items in a ListView, I thought this would be really easy, but I can't seem to work it out.
The ListView is bound to an observable collection view model... the items in the list view are grouped based on certain properties by setting up a CollectionViewSource and setting up GroupDescriptions:
BrowserItemCollectionView = CollectionViewSource.GetDefaultView(_browser);
//Set up filter
BrowserItemCollectionView.Filter = FilterBrowserItems;
//Set up grouping
PropertyGroupDescription L1PGD = new PropertyGroupDescription(nameof(BrowserItem.L1group));
PropertyGroupDescription L2PGD = new PropertyGroupDescription(nameof(BrowserItem.L2group));
PropertyGroupDescription L3PGD = new PropertyGroupDescription(nameof(BrowserItem.L3group));
BrowserItemCollectionView.GroupDescriptions.Add(L1PGD);
BrowserItemCollectionView.GroupDescriptions.Add(L2PGD);
BrowserItemCollectionView.GroupDescriptions.Add(L3PGD);
I then sort the groups with:
//Setup Sorting
BrowserItemCollectionView.SortDescriptions.Add(new SortDescription(nameof(BrowserItem.L1group), ListSortDirection.Descending));
BrowserItemCollectionView.SortDescriptions.Add(new SortDescription(nameof(BrowserItem.L2group), ListSortDirection.Descending));
BrowserItemCollectionView.SortDescriptions.Add(new SortDescription(nameof(BrowserItem.L3group), ListSortDirection.Descending));
This works well and sorts the groupings in Descending order.
What I now what to do is sort the actual items - the objects that are the lowest levels of the tree views.
What method do I use to sort the actual list view items?
Thanks.
What method do I use to sort the actual list view items?
You just add another SortDescription for a property of the "lowest level" item to BrowserItemCollectionView.SortDescriptions:
BrowserItemCollectionView.SortDescriptions.Add(
new SortDescription("PropertyOfItem", ListSortDirection.Descending));

WPF MVVM : get a collection of cell-data corresponds to a column

Is there any way to get DataGridColumns cells-data as a collection of cell-data corresponds to this column?
Note that I'm using MVVM and my datagrid is being dynamically built by DataGridColumn collection!
Thanks!
If you are really using MVVM, then you will know that you should have all of the data that is displayed in the view in your related view model. If that is correct, then you will have a collection that is data bound to the DataGrid.ItemsSource property. As we work with data in WPF and not UI elements, then you can get a collection that contains all of the values from one column using LinQ.
Let's say that you have a column (and therefore a property of your data type) that you want to single out. Let's say that that property is a string and named Name. You can gather all of the values of that property from each item in the collection like this:
List<string> names = yourCollection.Select(i => i.Name).ToList();
If it were an int property named Age, you could do this... and so on:
List<int> ages = yourCollection.Select(i => i.Age).ToList();

Where the combobox bound items are coming from?

May be it's a silly (or more than trivial) kinda question, but it seems i just don't know the answer. Here's the case -
I assigned a UserList as the ItemsSource of a combobox. So what i did essentially is assigning a reference type to another.
I cleared the UserList. So now i get the Count of the ItemsSource 0 as well.
I still get the items present in my combobox. And i also can cast the SelectedItem of the combobox to a User object.
Here's the complete code -
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
public partial class MainWindow : Window
{
private List<User> _userList;
public MainWindow()
{
InitializeComponent();
_userList = new List<User>()
{
new User() {Id = 1, Name = "X"},
new User() {Id = 2, Name = "Y"},
new User() {Id = 3, Name = "Z"}
};
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.comboBox1.ItemsSource = _userList;
this.comboBox1.DisplayMemberPath = "Name";
}
private void button1_Click(object sender, RoutedEventArgs e)
{
_userList.Clear();
/* ItemsSource is cleared as well*/
IEnumerable userList = this.comboBox1.ItemsSource;
/*I can still get my User*/
User user = this.comboBox1.SelectedItem as User;
}
}
So, where the items are coming from? What actually happens under-the-hood when i make such binding? Does the control have some kind of cache? It's a royal pain to realize not having such basic ideas. Can anybody explain the behind-the-scene detail?
EDIT : I wrote the code in WPF, but i have the same question for WinForms Combobox.
EDIT : Doesn't a combobox display its items from it's in-memory Datasource? When that datasource contains 0 items, how does it display the items?
When you set an ItemsSource of any ItemsControl it copies the ref to the list into its Items property. Then it subscribes to the OnCollectionChanged event, and creates a CollectionView object. So, on the screen you can see that collectionView.
as I have found in source code ItemCollection holds two lists:
internal void SetItemsSource(IEnumerable value)
{
//checks are missed
this._itemsSource = value;
this.SetCollectionView(CollectionViewSource.GetDefaultCollectionView((object) this._itemsSource, this.ModelParent));
}
How could you get SelectedItem?
This is my assumption from quick look into the source code:
ItemsControl has a collection of "views" and each View sholud store a ref to the item (User instance), because it has to draw data on the screen. So, when you call SelectedItem it returns a saved ref.
Upd about references
Assume there is an User instance. It has the adress 123 in memory. There is a list. It stores references. One of them is 123.
When you set an ItemsSource ItemsControl saves a reference to the list, and creates a Views collection. Each view stores a references to an item. One view stores an address 123.
Then you cleared a list of users. Now list doesn't contains any references to Users. But in memory there is an adrress 123 and there is an instance of User by this adress. Garbage Collector doesn't destroy it, because View has a reference to it.
When you get SelectedItem it returns User instance from the 123 adress.
var user = new User();
var list = new List<User>();
list.Add(user);
list.Clear();
Console.WriteLine(list.Count()); //prints 0 - list is empty
Console.WriteLine(user == null); //prints false. - user instance is sill exists;
In answer to your comment to #GazTheDestroyer ("... why it doesn't get cleared, and how it holds the items?")
In WPF, when you set the ItemsSource property of an ItemsControl, the control will wrap the list of items in a CollectionView, which is a collection type optimised for use by the UI framework. This CollectionView is assigned to the Items property of the control and is what the display-drawing code actually works from. As you see, this collection is entirely separate of the object you originally assigned to ItemsSource, and so there is no propogation of changes from one to the other. This is why the items are still in the control when you clear the original list: the control is ignoring the original list, and has its own list that contains your objects.
It's for this reason that an ItemsSource value needs to raise events - specifically INotifyCollectionChanged.NotifyCollectionChanged - so that the control knows to refresh the Items list. ObservableCollection implements this interface and raises the correct event, and so the functionality works as expected.
It's hugely important to note that this is nothing like what happens in WinForms, which is why I've been pressing you for the clarification.
EDIT: To clarify, there is no "deep copy." The code that is happening is similar in principle to the following:
private List<object> myCopy;
public void SetItemsSource(List<object> yourCopy)
{
myCopy = new List<object>();
foreach (var o in yourCopy)
{
myCopy.Add(o);
}
}
Once this code has run, there's only one copy of every item in your list. But each of the items is in both of the lists. If you change, clear or otherwise manipulate yourCopy, myCopy knows nothing about it. You cannot "destroy" any of the objects that are within the list my clearing yourCopy - all you do is release your own reference to them.
Assuming you are using WPF:
List<User> doesn't fire any event that the UI will recognise to refresh itself. If you use ObservableCollection<User> instead, your code will work.
The key difference is that ObservableCollection implements INotifyCollectionChanged, which allows the UI to recognise that the content of the collection has changed, and thus refresh the content of the ComboBox.
(Note that this does not work in WinForms. In WinForms you can set the DataSource property of the control, but the same ObservableCollection trick does not work here.)
When you set a collection reference to ItemsControl, all the combo gets is a reference, that it knows is enumerable.
It will enumerate the reference and display the items. Whether it does a deep copy or shallow copy is irrelevant, all it has is a reference (memory address effectively).
If you change your collection in some way, the combo has no way of knowing unless you tell it somehow. The reference (address) hasn't changed, everything looks the same to the combo. You seem to be thinking that the object is somehow "live" and the combo can watch the memory changing or something? This isn't the case. All it has is a reference that it can enumerate over. The contents can change but without some trigger the combo doesn't know that, and so will sit doing nothing.
ObservableCollection is designed to overcome this. It implements INotifyCollectionChanged that fires events when it changes, so the Combo knows that it must update its display.

Update item in BindableCollection with notify ICollectionView

Hi I bind collection from Caliburn Micro on ListBox control in view. Here is it.
public BindableCollection<UserInfo> Friends
{
get { return _friends; }
set
{
_friends = value;
NotifyOfPropertyChange(() => Friends);
}
}
ListBox items is type of UserInfo.
Hi I sort and group listbox items, I use CollectioView on this purpose.
When I initialize ListBox I sort and group items with this method.
private ICollectionView _currentView;
//...
private void SortContactList()
{
_currentView = CollectionViewSource.GetDefaultView(Friends);
_currentView.GroupDescriptions.Add(new PropertyGroupDescription("TextStatus"));
_currentView.SortDescriptions.Add(new SortDescription("TextStatus", ListSortDirection.Ascending));
_currentView.SortDescriptions.Add(new SortDescription("Nick", ListSortDirection.Ascending));
}
TextStatus and Nick are properties of userInfo class.
When I update values of item in bindable collection Friend I would like have a way how notify collection view about this change. Because I need move item to right/good group.
for example
Friend[0].TextStatus = "Ofline" -> is in offline group
I change value on online;
Friend[0].TextStatus="Online" -> move in online group
and here I want notify collection view (_currentView) about change on Friends collection.
I had the same issue when I created an application that had a table with the Rating column.
I wondered why row doesn't move up when I change rating, and in the end I used the Refresh method.
For your example:
Friend[0].TextStatus="Online" -> move in online group
_currentView.Refresh();
Fortunately, performance problems didn't occur, so now I use this solution in similar situations.

problem binding ListBox on ObservableCollection<T>

I have a strange "problem". Could someone explain me why :
If I have in an ObservableCollection, twice (or more time) an item with the same value, then the selections of those values in the ListBox won't work properly ?
In fact, what the ListBox is doing when I click on an item(Even in single item selection) : It selects the first item from the ObservableCollection collection with a matching value. so in the case if multiple items with same value are in the collection, then only the first one will be selected !
Because objects you entered to collection have same references. you should create new instances in each case or override Equal function and write your logic for identifying items. WPF ListBox calls Object.Equal function to identify if the items are same.
Hope this helps
You need to create a new object to hold each object.
I.e.
MyCollection.Add(new MyContainer() { Data = myObject } );
This way the listbox will select the objects properly as it has unique containers.
This would be implicit if you were using ViewModels

Resources