UWP ObservableCollection排序和分组

在UWP应用程序中,如何对ObservableCollection进行分组和排序并保持所有实时通知的良好性?

在我看过的大多数简单的UWP示例中,通常有一个ViewModel公开一个ObservableCollection,然后绑定到View中的ListView。 在ObservableCollection中添加或删除项目时,ListView会通过对INotifyCollectionChanged通知作出反应来自动反映更改。 在未分类或未分组的ObservableCollection的情况下,这一切都正常,但如果需要对集合进行排序或分组,似乎没有明显的方法来保留更新通知。 更重要的是,动态更改排序或组顺序似乎会引发重大的实施问题。

++

假设您有一个现有的datacache后端,该后端公开了一个非常简单的类Contact的ObservableCollection。

public class Contact { public string FirstName { get; set; } public string LastName { get; set; } public string State { get; set; } } 

此ObservableCollection随时间而变化,我们希望在视图中呈现实时分组和排序列表,以响应datacache中的更改进行更新。 我们还希望为用户提供在运行时切换LastName和State之间的分组的选项。

++

在WPF世界中,这是相对微不足道的。 我们可以创建一个简单的ViewModel,引用数据缓存,按原样显示缓存的Contacts集合。

 public class WpfViewModel { public WpfViewModel() { _cache = GetCache(); } Cache _cache; public ObservableCollection Contacts { get { return _cache.Contacts; } } } 

然后我们可以将它绑定到一个视图,我们将CollectionViewSource和Sort和Group定义实现为XAML资源。

                                              

然后当用户点击窗口底部的GroupBy按钮时,我们可以在代码隐藏中动态分组和排序。

 private void InitialGroupClick(object sender, RoutedEventArgs e) { var cvs = FindResource("cvs") as CollectionViewSource; var initialGroup = (PropertyGroupDescription)FindResource("initialgroup"); var firstSort = (SortDescription)FindResource("firstsort"); var lastSort = (SortDescription)FindResource("lastsort"); using (cvs.DeferRefresh()) { cvs.GroupDescriptions.Clear(); cvs.SortDescriptions.Clear(); cvs.GroupDescriptions.Add(initialGroup); cvs.SortDescriptions.Add(lastSort); cvs.SortDescriptions.Add(firstSort); } } private void StateGroupClick(object sender, RoutedEventArgs e) { var cvs = FindResource("cvs") as CollectionViewSource; var stateGroup = (PropertyGroupDescription)FindResource("stategroup"); var stateSort = (SortDescription)FindResource("statesort"); var lastSort = (SortDescription)FindResource("lastsort"); var firstSort = (SortDescription)FindResource("firstsort"); using (cvs.DeferRefresh()) { cvs.GroupDescriptions.Clear(); cvs.SortDescriptions.Clear(); cvs.GroupDescriptions.Add(stateGroup); cvs.SortDescriptions.Add(stateSort); cvs.SortDescriptions.Add(lastSort); cvs.SortDescriptions.Add(firstSort); } } 

这一切都很好,并且随着数据缓存集合的更改,项目会自动更新。 Listview分组和选择不受集合更改的影响,并且新的联系人项目已正确分组。在运行时,用户可以在State和LastName之间交换分组。

++

在UWP世界中,CollectionViewSource不再具有GroupDescriptions和SortDescriptions集合,并且需要在ViewModel级别执行排序/分组。 我找到的最接近可行解决方案的方法是微软的样本包

https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/XamlListView

和这篇文章

http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping

其中ViewModel使用Linq对ObservableCollection进行分组,并将其作为分组项的ObservableCollection呈现给视图

 public ObservableCollection GroupedContacts { ObservableCollection groups = new ObservableCollection(); var query = from item in _cache.Contacts group item by item.LastName[0] into g orderby g.Key select new { GroupName = g.Key, Items = g }; foreach (var g in query) { GroupInfoList info = new GroupInfoList(); info.Key = g.GroupName; foreach (var item in g.Items) { info.Add(item); } groups.Add(info); } return groups; } 

其中GroupInfoList定义为

 public class GroupInfoList : List { public object Key { get; set; } } 

这至少使我们在View中显示了一个分组集合,但是对datacache集合的更新不再实时反映。 我们可以捕获datacache的CollectionChanged事件并在viewmodel中使用它来刷新GroupedContacts集合,但是这会为datacache中的每个更改创建一个新集合,导致ListView闪烁并重置选择等,这显然是次优的。

另外,即时交换分组似乎需要为每个分组场景完全单独的ObservableCollection分组项,并且要在运行时交换ListView的ItemSource绑定。

我在UWP环境中看到的其余部分看起来非常有用,所以我很惊讶地找到了像分组和排序列表一样重要的东西……

谁知道如何正确地做到这一点?

我已经开始组建一个名为GroupedObservableCollection的库,它为我的一个应用程序执行这些操作。

我需要解决的一个关键问题是刷新用于创建组的原始列表,即我不希望用户搜索具有稍微不同的标准来导致整个列表被刷新,只是差异。

目前的forms,它可能不会立即回答你所有的排序问题,但对其他人来说这可能是一个很好的起点。

到目前为止,尽力而为使用以下助手类ObservableGroupingCollection

 public class ObservableGroupingCollection where K : IComparable { public ObservableGroupingCollection(ObservableCollection collection) { _rootCollection = collection; _rootCollection.CollectionChanged += _rootCollection_CollectionChanged; } ObservableCollection _rootCollection; private void _rootCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { HandleCollectionChanged(e); } ObservableCollection> _items; public ObservableCollection> Items { get { return _items; } } IComparer _sortOrder; Func _groupFunction; public void ArrangeItems(IComparer sortorder, Func group) { _sortOrder = sortorder; _groupFunction = group; var temp = _rootCollection .OrderBy(i => i, _sortOrder) .GroupBy(_groupFunction) .ToList() .Select(g => new Grouping(g.Key, g)); _items = new ObservableCollection>(temp); } private void HandleCollectionChanged(NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add) { var item = (T)(e.NewItems[0]); var value = _groupFunction.Invoke(item); // find matching group if exists var existingGroup = _items.FirstOrDefault(g => g.Key.Equals(value)); if (existingGroup == null) { var newlist = new List(); newlist.Add(item); // find first group where Key is greater than this key var insertBefore = _items.FirstOrDefault(g => ((g.Key).CompareTo(value)) > 0); if (insertBefore == null) { // not found - add new group to end of list _items.Add(new Grouping(value, newlist)); } else { // insert new group at this index _items.Insert(_items.IndexOf(insertBefore), new Grouping(value, newlist)); } } else { // find index to insert new item in existing group int index = existingGroup.ToList().BinarySearch(item, _sortOrder); if (index < 0) { existingGroup.Insert(~index, item); } } } else if (e.Action == NotifyCollectionChangedAction.Remove) { var item = (T)(e.OldItems[0]); var value = _groupFunction.Invoke(item); var existingGroup = _items.FirstOrDefault(g => g.Key.Equals(value)); if (existingGroup != null) { // find existing item and remove var targetIndex = existingGroup.IndexOf(item); existingGroup.RemoveAt(targetIndex); // remove group if zero items if (existingGroup.Count == 0) { _items.Remove(existingGroup); } } } } } 

其中genericsGrouping类(它本身公开了一个ObservableCollection)来自本文

http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping

要进行工作演示: –

从新的UWP Blank应用程序中,添加上面的ObservableGroupingCollection类。 然后在同一名称空间中添加另一个类文件并添加以下所有类

 // Data models public class Contact { public string FirstName { get; set; } public string LastName { get; set; } public string State { get; set; } } public class DataPool { public static string GenerateFirstName(Random random) { List names = new List() { "Lilly", "Mukhtar", "Sophie", "Femke", "Abdul-Rafi", "Mariana", "Aarif", "Sara", "Ibadah", "Fakhr", "Ilene", "Sardar", "Hanna", "Julie", "Iain", "Natalia", "Henrik", "Rasa", "Quentin", "Gadi", "Pernille", "Ishtar", "Jimmy", "Justine", "Lale", "Elize", "Randy", "Roshanara", "Rajab", "Marcus", "Mark", "Alima", "Francisco", "Thaqib", "Andreas", "Marianna", "Amalie", "Rodney", "Dena", "Amar", "Anna", "Nasreen", "Reema", "Tomas", "Filipa", "Frank", "Bari'ah", "Parvaiz", "Jibran", "Tomas", "Elli", "Carlos", "Diego", "Henrik", "Aruna", "Vahid", "Eliana", "Roxanne", "Amanda", "Ingrid", "Wesley", "Malika", "Basim", "Eisa", "Alina", "Andreas", "Deeba", "Diya", "Parveen", "Bakr", "Celine", "Daniel", "Mattheus", "Edmee", "Hedda", "Maria", "Maja", "Alhasan", "Alina", "Hedda", "Vanja", "Robin", "Victor", "Aaftab", "Guilherme", "Maria", "Kai", "Sabien", "Abdel", "Jason", "Bahaar", "Vasco", "Jibran", "Parsa", "Catalina", "Fouad", "Colette", "John", "Fred", "James", "Harry", "Ben", "Steven", "Philip", "Dougal", "Jasper", "Elliott", "Charles", "Gerty", "Sarah", "Sonya", "Svetlana", "Dita", "Karen", "Christine", "Angela", "Heather", "Spence", "Graham", "David", "Bernie", "Darren", "Lester", "Vince", "Colin", "Bernhard", "Dieter", "Norman", "William", "Nigel", "Nick", "Nikki", "Trent", "Devon", "Steven", "Eric", "Derek", "Raymond", "Craig" }; return names[random.Next(0, names.Count)]; } public static string GenerateLastName(Random random) { List lastnames = new List() { "Carlson", "Attia", "Quincey", "Hollenberg", "Khoury", "Araujo", "Hakimi", "Seegers", "Abadi", "Krommenhoek", "Siavashi", "Kvistad", "Vanderslik", "Fernandes", "Dehmas", "Sheibani", "Laamers", "Batlouni", "Lyngvær", "Oveisi", "Veenhuizen", "Gardenier", "Siavashi", "Mutlu", "Karzai", "Mousavi", "Natsheh", "Nevland", "Lægreid", "Bishara", "Cunha", "Hotaki", "Kyvik", "Cardoso", "Pilskog", "Pennekamp", "Nuijten", "Bettar", "Borsboom", "Skistad", "Asef", "Sayegh", "Sousa", "Miyamoto", "Medeiros", "Kregel", "Shamoun", "Behzadi", "Kuzbari", "Ferreira", "Barros", "Fernandes", "Xuan", "Formosa", "Nolette", "Shahrestaani", "Correla", "Amiri", "Sousa", "Fretheim", "Van", "Hamade", "Baba", "Mustafa", "Bishara", "Formo", "Hemmati", "Nader", "Hatami", "Natsheh", "Langen", "Maloof", "Patel", "Berger", "Ostrem", "Bardsen", "Kramer", "Bekken", "Salcedo", "Holter", "Nader", "Bettar", "Georgsen", "Cuninho", "Zardooz", "Araujo", "Batalha", "Antunes", "Vanderhoorn", "Srivastava", "Trotter", "Siavashi", "Montes", "Sherzai", "Vanderschans", "Neves", "Sarraf", "Kuiters", "Hestoe", "Cornwall", "Paisley", "Cooper", "Jakoby", "Smith", "Davies", "Jonas", "Bowers", "Fernandez", "Perez", "Black", "White", "Keller", "Hernandes", "Clinton", "Merryweather", "Freeman", "Anguillar", "Goodman", "Hardcastle", "Emmott", "Kirkby", "Thatcher", "Jamieson", "Spender", "Harte", "Pinkman", "Winterman", "Knight", "Taylor", "Wentworth", "Manners", "Walker", "McPherson", "Elder", "McDonald", "Macintosh", "Decker", "Takahashi", "Wagoner" }; return lastnames[random.Next(0, lastnames.Count)]; } public static string GenerateState(Random random) { List states = new List() { "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District Of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming" }; return states[random.Next(0, states.Count)]; } } public class Cache { public Cache() { InitializeCacheData(); SimulateLiveChanges(new TimeSpan(0, 0, 1)); } public ObservableCollection Contacts { get; set; } private static Random rnd = new Random(); private void InitializeCacheData() { Contacts = new ObservableCollection(); var i = 0; while (i < 5) { Contacts.Add(new Contact() { FirstName = DataPool.GenerateFirstName(rnd), LastName = DataPool.GenerateLastName(rnd), State = DataPool.GenerateState(rnd) }); i++; } } private async void SimulateLiveChanges(TimeSpan MyInterval) { double MyIntervalSeconds = MyInterval.TotalSeconds; while (true) { await Task.Delay(MyInterval); //int addOrRemove = rnd.Next(1, 10); //if (addOrRemove > 3) //{ // add item Contacts.Add(new Contact() { FirstName = DataPool.GenerateFirstName(rnd), LastName = DataPool.GenerateLastName(rnd), State = DataPool.GenerateState(rnd) }); //} //else //{ // // remove random item // if (Contacts.Count > 0) // { // Contacts.RemoveAt(rnd.Next(0, Contacts.Count - 1)); // } //} } } } // ViewModel public class ViewModel : BaseViewModel { public ViewModel() { _groupingCollection = new ObservableGroupingCollection(new Cache().Contacts); _groupingCollection.ArrangeItems(new StateSorter(), (x => x.State)); NotifyPropertyChanged("GroupedContacts"); } ObservableGroupingCollection _groupingCollection; public ObservableCollection> GroupedContacts { get { return _groupingCollection.Items; } } // swap grouping commands private ICommand _groupByStateCommand; public ICommand GroupByStateCommand { get { if (_groupByStateCommand == null) { _groupByStateCommand = new RelayCommand( param => GroupByState(), param => true); } return _groupByStateCommand; } } private void GroupByState() { _groupingCollection.ArrangeItems(new StateSorter(), (x => x.State)); NotifyPropertyChanged("GroupedContacts"); } private ICommand _groupByNameCommand; public ICommand GroupByNameCommand { get { if (_groupByNameCommand == null) { _groupByNameCommand = new RelayCommand( param => GroupByName(), param => true); } return _groupByNameCommand; } } private void GroupByName() { _groupingCollection.ArrangeItems(new NameSorter(), (x => x.LastName.First().ToString())); NotifyPropertyChanged("GroupedContacts"); } } // View Model helpers public class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } public class RelayCommand : ICommand { readonly Action _execute; readonly Predicate _canExecute; public RelayCommand(Action execute) : this(execute, null) { } public RelayCommand(Action execute, Predicate canExecute) { if (execute == null) throw new ArgumentNullException("execute"); _execute = execute; _canExecute = canExecute; } public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); } public event EventHandler CanExecuteChanged { add { } remove { } } public void Execute(object parameter) { _execute(parameter); } } // Sorter classes public class NameSorter : Comparer { public override int Compare(Contact x, Contact y) { int result = x.LastName.First().CompareTo(y.LastName.First()); if (result != 0) { return result; } else { result = x.LastName.CompareTo(y.LastName); if (result != 0) { return result; } else { return x.FirstName.CompareTo(y.FirstName); } } } } public class StateSorter : Comparer { public override int Compare(Contact x, Contact y) { int result = x.State.CompareTo(y.State); if (result != 0) { return result; } else { result = x.LastName.CompareTo(y.LastName); if (result != 0) { return result; } else { return x.FirstName.CompareTo(y.FirstName); } } } } // Grouping class // credit // http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping public class Grouping : ObservableCollection { public K Key { get; private set; } public Grouping(K key, IEnumerable items) { Key = key; foreach (var item in items) { this.Items.Add(item); } } } 

最后,编辑MainPage如下

                                             

HandleCollectionChanged方法仅处理目前为止的Add / Remove,如果NotifyCollectionChangedEventArgs参数包含多个项目,则会分解(Existing ObservableCollection类一次只通知一个更改)

所以它可以正常工作,但这一切都让人觉得有点hacky。

改进的建议非常欢迎。