Skip to content

Instantly share code, notes, and snippets.

@jamie94bc
Last active January 21, 2016 13:54
Show Gist options
  • Save jamie94bc/6262479 to your computer and use it in GitHub Desktop.
Save jamie94bc/6262479 to your computer and use it in GitHub Desktop.
A early (largely untested), simple implementation of CollectionViewSource for use view models in portable class libraries.Does not yet support grouping
/// <summary>
/// A simple implementation of CollectionView / CollectionViewSource
/// for portable class libraries.
/// </summary>
/// <remarks>
/// Unfortunately due some Windows 8 oddness, we had to resort to using
/// an <see cref="ObservableCollection{T}"/> as the view. Probably something
/// to do with ObservableMap but ItemsControls and the inherited versions
/// don't subscribe to the <see cref="INotifyCollectionChanged.CollectionChanged"/>
/// event.
///
/// (Sept. 2013)
/// </remarks>
public class CollectionViewSource<T> : INotifyPropertyChanged, IEnumerable<T> {
private ObservableCollection<T> _source;
private Predicate<T> _filter;
private readonly ObservableCollection<SortDescription<T>> _sortDescriptions = new ObservableCollection<SortDescription<T>>();
private ObservableCollection<T> _view = new ObservableCollection<T>();
public ObservableCollection<T> Source {
get {
return _source;
}
set {
if (_source == value) return;
if (_source != null)
_source.CollectionChanged -= SourceOnCollectionChanged;
_source = value;
if (_source != null)
_source.CollectionChanged += SourceOnCollectionChanged;
OnSourceChanged();
}
}
public Predicate<T> Filter {
get { return this._filter; }
set {
if (_filter == value) return;
_filter = value;
OnFilterChanged();
}
}
public ICollection<SortDescription<T>> SortDescriptions {
get { return _sortDescriptions; }
}
public virtual ObservableCollection<T> View {
get { return _view; }
private set {
_view = value;
NotifyPropertyChanged();
}
}
public int Count {
get {
return _view != null ? _view.Count : 0;
}
}
private int _resetThreshold = 5;
/// <summary>
/// The number of items after which a single
/// <see cref="NotifyCollectionChangedAction.Reset"/> event
/// will be fired as opposed to multiple events.
/// </summary>
/// <remarks>
/// This is due to portable class library's insufficient support
/// for <see cref="NotifyCollectionChangedEventArgs"/>.
/// </remarks>
public virtual int ResetThreshold {
get { return _resetThreshold; }
set { _resetThreshold = value; }
}
#region Constructors
public CollectionViewSource(ObservableCollection<T> source) {
this.Init(source);
}
public CollectionViewSource(ObservableCollection<T> source, params SortDescription<T>[] sortDescriptions) {
if (source == null)
throw new ArgumentNullException("source");
if (sortDescriptions == null)
throw new ArgumentNullException("sortDescriptions");
this.SortDescriptions.AddRange(sortDescriptions);
this.Init(source);
}
public CollectionViewSource(ObservableCollection<T> source, Predicate<T> filter, params SortDescription<T>[] sortDescriptions) {
if (source == null)
throw new ArgumentNullException("source");
if (filter == null)
throw new ArgumentNullException("filter");
this._filter = filter;
this.SortDescriptions.AddRange(sortDescriptions);
this.Init(source);
}
private void Init(ObservableCollection<T> source) {
this.Source = source; // Source prop calls EnsureView()
this._sortDescriptions.CollectionChanged += SortDescriptionsOnCollectionChanged;
}
#endregion
/// <summary>
/// Resets the filter and sort descriptions to the given
/// values.
/// </summary>
/// <remarks>
/// Much more efficient than clearing/adding to the
/// sort descriptions collection by hand.
/// </remarks>
public void SetFilterAndSort(Predicate<T> filter, params SortDescription<T>[] sortDescriptions) {
this._sortDescriptions.CollectionChanged -= SortDescriptionsOnCollectionChanged;
this._sortDescriptions.Clear();
this._sortDescriptions.AddRange(sortDescriptions);
this.Filter = filter;
this._sortDescriptions.CollectionChanged += SortDescriptionsOnCollectionChanged;
}
#region OnChanged
private void SourceOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
var toAdd = new List<T>();
var toRemove = new List<T>();
switch (e.Action) {
case NotifyCollectionChangedAction.Add:
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var item in e.NewItems.Cast<T>().Where(MatchFilter)) {
toAdd.Add(item);
}
break;
case NotifyCollectionChangedAction.Replace:
toRemove.AddRange(e.OldItems.Cast<T>());
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var item in e.NewItems.Cast<T>().Where(MatchFilter)) {
toAdd.Add(item);
}
break;
case NotifyCollectionChangedAction.Remove:
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var item in e.OldItems.Cast<T>()) {
toRemove.Add(item);
}
break;
case NotifyCollectionChangedAction.Reset:
EnsureView();
break;
}
PerformAddAndRemove(toAdd, toRemove);
}
private void OnSourceChanged() {
EnsureView();
}
private void OnFilterChanged() {
EnsureView();
}
private void SortDescriptionsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
this.ReSort();
}
#endregion
private void EnsureView() {
var toAdd = Source.Where(MatchFilter).Where(item => !this.View.Contains(item)).ToList();
var toRemove = this._view.Where(x => !Source.Contains(x) || !MatchFilter(x)).ToList();
PerformAddAndRemove(toAdd, toRemove);
}
private void PerformAddAndRemove(IList<T> toAdd, IList<T> toRemove) {
if ((toAdd.Count + toRemove.Count) > ResetThreshold) {
var newCollection = this._view.ToList();
foreach (var item in toAdd) {
AddSorted(item, newCollection);
}
foreach (var item in toRemove) {
newCollection.Remove(item);
}
this.View = newCollection.ToObservableCollection();
}
else {
foreach (var item in toAdd) {
AddSorted(item);
}
foreach (var item in toRemove) {
this.View.Remove(item);
}
}
// ReSharper disable once ExplicitCallerInfoArgument
NotifyPropertyChanged("Count");
}
public void NotifyItemsSourceChanged() {
this.EnsureView();
}
public void ReSort() {
// Unfortunately PCL's don't implement NotifyCollectionChangedEventArgs.Move so we have to reset the
// list whenever this changes :(
var sortedItems = this.Source.ToList();
sortedItems.Sort(SortComparison);
int notifyCount = 0;
var viewList = this._view.ToList();
foreach (var item in viewList) {
int sortedIndex = sortedItems.IndexOf(item);
if (sortedIndex != this._view.IndexOf(item)) {
this._view.Remove(item);
this._view.Insert(sortedIndex, item);
notifyCount++;
if (notifyCount > ResetThreshold) {
break;
}
}
}
if (notifyCount > ResetThreshold) {
this.View = sortedItems.ToObservableCollection();
}
}
#region Utils
private bool MatchFilter(T obj) {
return Filter == null || Filter(obj);
}
private Comparison<T> SortComparison {
get {
return (x, y) => {
// x > = 1, y > = -1
int move = 0;
foreach (var sd in SortDescriptions) {
var xPropVal = sd.Property(x);
var yPropVal = sd.Property(y);
if (xPropVal == null && yPropVal == null) {
move = 0;
}
else if (xPropVal == null) {
move = 1;
}
else if (yPropVal == null) {
move = -1;
}
else {
move = sd.Property(x).CompareTo(sd.Property(y));
}
if (sd.Order == SortOrder.Descending) {
move *= -1;
}
if (move != 0) return move;
}
return move;
};
}
}
private int AddSorted(T item, IList<T> collection = null) {
var sortedList = (collection ?? View).ToList();
sortedList.Add(item);
sortedList.Sort(SortComparison);
int idx = sortedList.IndexOf(item);
(collection ?? View).Insert(idx, item);
return idx;
}
#endregion
#region Implementation of INotifyPropertyChanged
protected virtual event PropertyChangedEventHandler PropertyChanged;
event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged {
add {
PropertyChanged += value;
}
remove {
PropertyChanged -= value;
}
}
protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region Implementation of IEnumerable
public IEnumerator<T> GetEnumerator() {
return this.View.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
#endregion
}
public class SortDescription<T> {
public Func<T, IComparable> Property { get; private set; }
public SortOrder Order { get; private set; }
public SortDescription(Func<T, IComparable> property, SortOrder order = SortOrder.Ascending) {
if (property == null)
throw new ArgumentNullException("property");
this.Property = property;
this.Order = order;
}
}
public enum SortOrder {
Ascending,
Descending
}
@rb1234
Copy link

rb1234 commented Mar 12, 2014

Appreciate this is fairly old code and untested, but as a warning to others: EnsureView should be performing the following two tasks:
(1) ensure all source values matching the filter are present in the view
(2) ensure no view values do not match the filter or are not present in the source

The current implementation fails to correctly perform (2). There is also some scope for improvement when adding multiple values, as this is inefficient and the index returned when calling AddSource multiple times may have been invalidated - the handling of new items should be fairly atomic.

@jamie94bc
Copy link
Author

@rb1234 there is definitely scope for improvement here however I have updated it with the latest version we are using.

(2) has been fixed and the View property is of type ObservableCollection<T> due to some oddness we encountered on Windows 8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment