UWPアプリで、ObservableCollectionをグループ化して並べ替え、すべてのライブ通知の品質を維持するにはどうすればよいですか?
私が見た最も単純なUWPの例では、通常、ObservableCollectionを公開するViewModelがあり、これはビュー内のListViewにバインドされます。アイテムがObservableCollectionに追加または削除されると、ListViewはINotifyCollectionChanged通知に反応して、変更を自動的に反映します。これは、ソートされていない、またはグループ化されていないObservableCollectionの場合はすべて正常に機能しますが、コレクションをソートまたはグループ化する必要がある場合、更新通知を保持する明確な方法はないようです。さらに、ソートまたはグループの順序をその場で変更すると、実装上の重大な問題が発生するようです。
++
非常に単純なクラスContactのObservableCollectionを公開する既存のデータキャッシュバックエンドがあるシナリオを考えてみましょう。
public class Contact
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string State { get; set; }
}
このObservableCollectionは時間の経過とともに変化するため、データキャッシュの変化に応じて更新されるビューに、グループ化およびソートされたリアルタイムのリストを表示する必要があります。また、LastNameとStateの間でグループ化をオンザフライで切り替えるオプションをユーザーに提供したいと思います。
++
WPFの世界では、これは比較的簡単です。キャッシュのContactsコレクションをそのまま表示するデータキャッシュを参照する単純なViewModelを作成できます。
public class WpfViewModel
{
public WpfViewModel()
{
_cache = GetCache();
}
Cache _cache;
public ObservableCollection<Contact> Contacts
{
get { return _cache.Contacts; }
}
}
次に、これをビューにバインドして、CollectionViewSourceおよびSort andGroup定義をXAMLリソースとして実装できます。
<Window .....
xmlns:scm="clr-namespace:System.ComponentModel;Assembly=WindowsBase">
<Window.DataContext>
<local:WpfViewModel />
</Window.DataContext>
<Window.Resources>
<CollectionViewSource x:Key="cvs" Source="{Binding Contacts}" />
<PropertyGroupDescription x:Key="stategroup" PropertyName="State" />
<PropertyGroupDescription x:Key="initialgroup" PropertyName="LastName[0]" />
<scm:SortDescription x:Key="statesort" PropertyName="State" Direction="Ascending" />
<scm:SortDescription x:Key="lastsort" PropertyName="LastName" Direction="Ascending" />
<scm:SortDescription x:Key="firstsort" PropertyName="FirstName" Direction="Ascending" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Source={StaticResource cvs}}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding LastName}" />
<TextBlock Text="{Binding FirstName}" Grid.Column="1" />
<TextBlock Text="{Binding State}" Grid.Column="2" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid Background="Gainsboro">
<TextBlock FontWeight="Bold"
FontSize="14"
Margin="10,2"
Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Content="Group By Initial" Click="InitialGroupClick" />
<Button Content="Group By State" Click="StateGroupClick" />
</StackPanel>
</Grid>
</Window>
次に、ユーザーがウィンドウの下部にある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);
}
}
これはすべて正常に機能し、データキャッシュコレクションが変更されるとアイテムが自動的に更新されます。リストビューのグループ化と選択はコレクションの変更による影響を受けず、新しい連絡先アイテムは正しくグループ化されます。グループ化は、実行時にユーザーが最初にStateとLastNameの間で交換できます。
++
UWPの世界では、CollectionViewSourceにGroupDescriptionsコレクションとSortDescriptionsコレクションがなくなり、並べ替え/グループ化をViewModelレベルで実行する必要があります。私が見つけた実行可能なソリューションへの最も近いアプローチは、Microsoftのサンプルパッケージのラインに沿ったものです。
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<GroupInfoList> GroupedContacts
{
ObservableCollection<GroupInfoList> groups = new ObservableCollection<GroupInfoList>();
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<object>
{
public object Key { get; set; }
}
これにより、少なくともグループ化されたコレクションがビューに表示されますが、データキャッシュコレクションの更新はリアルタイムに反映されなくなります。データキャッシュのCollectionChangedイベントをキャプチャし、それをビューモデルで使用してGroupedContactsコレクションを更新することもできますが、これにより、データキャッシュの変更ごとに新しいコレクションが作成され、ListViewがちらつき、選択がリセットされるなど、明らかに最適ではありません。
また、その場でグループ化を交換するには、グループ化シナリオごとに、および実行時にListViewのItemSourceバインディングを交換するために、グループ化されたアイテムの完全に別個のObservableCollectionが必要になるようです。
UWP環境について私が見た残りの部分は非常に役立つように思われるので、リストをグループ化して並べ替えるのと同じくらい重要なものを見つけて、障害物を投げ出すことに驚いています...
誰かがこれを正しく行う方法を知っていますか?
GroupedObservableCollection というライブラリを作成し始めました。このライブラリは、私のアプリの1つに対してこれらの行に沿って何かを実行します。
私が解決する必要のある重要な問題の1つは、グループの作成に使用された元のリストの更新でした。つまり、ユーザーがわずかに異なる条件で検索してリスト全体が更新されることを望まず、違いだけが更新されました。 。
現在の形式では、おそらく現時点ですべての並べ替えの質問に答えることはできませんが、他の人にとっては良い出発点になる可能性があります。
これまでのベストエフォートでは、次のヘルパークラスObservableGroupingCollectionを使用しています
public class ObservableGroupingCollection<K, T> where K : IComparable
{
public ObservableGroupingCollection(ObservableCollection<T> collection)
{
_rootCollection = collection;
_rootCollection.CollectionChanged += _rootCollection_CollectionChanged;
}
ObservableCollection<T> _rootCollection;
private void _rootCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
HandleCollectionChanged(e);
}
ObservableCollection<Grouping<K, T>> _items;
public ObservableCollection<Grouping<K, T>> Items
{
get { return _items; }
}
IComparer<T> _sortOrder;
Func<T, K> _groupFunction;
public void ArrangeItems(IComparer<T> sortorder, Func<T, K> group)
{
_sortOrder = sortorder;
_groupFunction = group;
var temp = _rootCollection
.OrderBy(i => i, _sortOrder)
.GroupBy(_groupFunction)
.ToList()
.Select(g => new Grouping<K, T>(g.Key, g));
_items = new ObservableCollection<Grouping<K, T>>(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<T>();
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<K, T>(value, newlist));
}
else
{
// insert new group at this index
_items.Insert(_items.IndexOf(insertBefore), new Grouping<K, T>(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);
}
}
}
}
}
ここで、一般的なGroupingクラス(それ自体がObservableCollectionを公開します)はこの記事からのものです
http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping
動作するデモを作成するには:-
新しいUWPブランクアプリケーションから、上記の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<string> names = new List<string>() { "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<string> lastnames = new List<string>() { "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<string> states = new List<string>() { "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<Contact> Contacts { get; set; }
private static Random rnd = new Random();
private void InitializeCacheData()
{
Contacts = new ObservableCollection<Contact>();
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<string, Contact>(new Cache().Contacts);
_groupingCollection.ArrangeItems(new StateSorter(), (x => x.State));
NotifyPropertyChanged("GroupedContacts");
}
ObservableGroupingCollection<string, Contact> _groupingCollection;
public ObservableCollection<Grouping<string, Contact>> 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<object> _execute;
readonly Predicate<object> _canExecute;
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> 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<Contact>
{
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<Contact>
{
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<K, T> : ObservableCollection<T>
{
public K Key { get; private set; }
public Grouping(K key, IEnumerable<T> items)
{
Key = key;
foreach (var item in items)
{
this.Items.Add(item);
}
}
}
最後に、MainPageを次のように編集します
<Page.DataContext>
<local:ViewModel />
</Page.DataContext>
<Page.Resources>
<CollectionViewSource
x:Key="cvs"
Source="{Binding GroupedContacts}"
IsSourceGrouped="True" />
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Source={StaticResource cvs}}"
x:Name="targetListBox">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding LastName}" />
<TextBlock Text="{Binding FirstName}" Grid.Column="1" />
<TextBlock Text="{Binding State}" Grid.Column="2" HorizontalAlignment="Right" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid Background="Gainsboro">
<TextBlock FontWeight="Bold"
FontSize="14"
Margin="10,2"
Text="{Binding Key}"/>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Content="Group By Initial" Command="{Binding GroupByNameCommand}" />
<Button Content="Group By State" Command="{Binding GroupByStateCommand}" />
</StackPanel>
</Grid>
HandleCollectionChangedメソッドは、これまでのところ追加/削除のみを処理し、NotifyCollectionChangedEventArgsパラメーターに複数の項目が含まれている場合は機能しなくなります(既存のObservableCollectionクラスは一度に1つずつ変更を通知します)
だからそれは大丈夫ですが、それはすべて一種のハッキーな感じがします。
改善のための提案は大歓迎です。