Skip to content

Instantly share code, notes, and snippets.

@praeclarum
Last active May 7, 2019 19:46
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save praeclarum/2df15b98f4576c1815a35cb60790e4e6 to your computer and use it in GitHub Desktop.
Save praeclarum/2df15b98f4576c1815a35cb60790e4e6 to your computer and use it in GitHub Desktop.
This class is a new collection that is given a function to retrieve its own data. It uses ListDiff to minimize updates.
using System;
using System.Collections.Specialized;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Linq;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Praeclarum
{
public class ObservableQuery<T> : IList<T>, IList, INotifyCollectionChanged
{
public event NotifyCollectionChangedEventHandler CollectionChanged = delegate {};
readonly Func<IEnumerable<T>> query;
// readonly ReaderWriterLockSlim itemsLock = new ReaderWriterLockSlim ();
SemaphoreSlim updateMutex = new SemaphoreSlim (1);
readonly List<T> items = new List<T> ();
Func<T, T, bool> match;
Timer throttleTimer;
const int ThrottleMillis = 100;
public ObservableQuery (Func<IEnumerable<T>> query, Func<T, T, bool> match = null)
{
// Console.WriteLine ("OC Created on Thread: {0}", Thread.CurrentThread.ManagedThreadId);
this.query = query;
this.match = match ?? (Func<T, T, bool>)((a, b) => a.Equals (b));
Refresh ();
}
public void Refresh ()
{
try {
throttleTimer = null;
LoadItems ();
} catch (Exception ex) {
Debug.WriteLine (ex);
if (!IgnoreRefreshExceptions)
throw;
}
}
public void ThrottledRefresh ()
{
if (throttleTimer == null) {
throttleTimer = new Timer (
x => {
try {
Refresh ();
} catch (Exception ex) {
Log.Error (ex);
}
},
null,
ThrottleMillis,
Timeout.Infinite);
} else {
throttleTimer.Change (ThrottleMillis, Timeout.Infinite);
}
}
void LoadItems ()
{
updateMutex.Wait ();
// Console.WriteLine ("OC LoadItems on Thread: {0}", Thread.CurrentThread.ManagedThreadId);
try {
var newItems = query ().ToList ();
var diff = new ListDiff<T,T> (items, newItems, match);
var adds = new List<T> ();
var removes = new List<T> ();
var p = 0;
var prevActionType = ListDiffActionType.Add;
for (var i = 0; i < diff.Actions.Count; i++) {
var a = diff.Actions [i];
var actionTypeChanged = i != 0 && a.ActionType != prevActionType;
if (actionTypeChanged) {
EmitCollectionChanged (adds, removes, ref p);
}
switch (a.ActionType) {
case ListDiffActionType.Add:
adds.Add (a.DestinationItem);
break;
case ListDiffActionType.Remove:
removes.Add (a.SourceItem);
break;
case ListDiffActionType.Update:
p++;
break;
}
prevActionType = a.ActionType;
}
EmitCollectionChanged (adds, removes, ref p);
} finally {
updateMutex.Release ();
}
}
public bool IgnoreRefreshExceptions = false;
void EmitCollectionChanged (List<T> adds, List<T> removes, ref int p)
{
if (adds.Count > 0) {
items.InsertRange (p, adds);
try {
CollectionChanged (this, new NotifyCollectionChangedEventArgs (
NotifyCollectionChangedAction.Add,
adds,
p));
} catch (Exception ex) {
Debug.WriteLine (ex);
if (!IgnoreRefreshExceptions)
throw;
}
p += adds.Count;
adds.Clear ();
}
if (removes.Count > 0) {
items.RemoveRange (p, removes.Count);
try {
CollectionChanged (this, new NotifyCollectionChangedEventArgs (
NotifyCollectionChangedAction.Remove,
removes,
p));
} catch (Exception ex) {
Debug.WriteLine (ex);
if (!IgnoreRefreshExceptions)
throw;
}
removes.Clear ();
}
}
#region IList implementation
public int IndexOf (T item)
{
// itemsLock.EnterReadLock ();
// try {
return items.IndexOf (item);
// } finally {
// itemsLock.ExitReadLock ();
// }
}
public void Insert (int index, T item)
{
throw new NotSupportedException ();
}
public void RemoveAt (int index)
{
throw new NotSupportedException ();
}
public T this [int index] {
get {
// itemsLock.EnterReadLock ();
// try {
return items [index];
// } finally {
// itemsLock.ExitReadLock ();
// }
}
// Analysis disable once ValueParameterNotUsed
set {
}
}
object IList.this [int index] {
get {
// itemsLock.EnterReadLock ();
// try {
return items [index];
// } finally {
// itemsLock.ExitReadLock ();
// }
}
// Analysis disable once ValueParameterNotUsed
set {
}
}
int IList.Add (object value)
{
throw new NotSupportedException ();
}
bool IList.Contains (object value)
{
// itemsLock.EnterReadLock ();
// try {
return ((IList)items).Contains (value);
// } finally {
// itemsLock.ExitReadLock ();
// }
}
int IList.IndexOf (object value)
{
// itemsLock.EnterReadLock ();
// try {
return ((IList)items).IndexOf (value);
// } finally {
// itemsLock.ExitReadLock ();
// }
}
void IList.Insert (int index, object value)
{
throw new NotSupportedException ();
}
void IList.Remove (object value)
{
throw new NotSupportedException ();
}
bool IList.IsFixedSize {
get {
return true;
}
}
#endregion
#region ICollection implementation
public void Add (T item)
{
throw new NotSupportedException ();
}
public void Clear ()
{
throw new NotSupportedException ();
}
public bool Contains (T item)
{
// itemsLock.EnterReadLock ();
// try {
return items.Contains (item);
// } finally {
// itemsLock.ExitReadLock ();
// }
}
public void CopyTo (T[] array, int arrayIndex)
{
// itemsLock.EnterReadLock ();
// try {
items.CopyTo (array, arrayIndex);
// } finally {
// itemsLock.ExitReadLock ();
// }
}
public bool Remove (T item)
{
throw new NotSupportedException ();
}
public int Count {
get {
// itemsLock.EnterReadLock ();
// try {
return items.Count;
// } finally {
// itemsLock.ExitReadLock ();
// }
}
}
public bool IsReadOnly {
get {
return true;
}
}
void ICollection.CopyTo (Array array, int index)
{
// itemsLock.EnterReadLock ();
// try {
((ICollection)items).CopyTo (array, index);
// } finally {
// itemsLock.ExitReadLock ();
// }
}
bool ICollection.IsSynchronized {
get {
return false;
}
}
object syncRoot = null;
object ICollection.SyncRoot {
get {
if (syncRoot == null)
syncRoot = new object ();
return syncRoot;
}
}
#endregion
#region IEnumerable implementation
public IEnumerator<T> GetEnumerator ()
{
// itemsLock.EnterReadLock ();
// try {
return items.ToList ().GetEnumerator ();
// } finally {
// itemsLock.ExitReadLock ();
// }
}
IEnumerator IEnumerable.GetEnumerator ()
{
// itemsLock.EnterReadLock ();
// try {
return items.ToList ().GetEnumerator ();
// } finally {
// itemsLock.ExitReadLock ();
// }
}
#endregion
}
public class ObservableCollectionQuery<T> : ObservableQuery<T>
{
readonly INotifyCollectionChanged notifier;
public ObservableCollectionQuery (object collection, Func<IEnumerable<T>> query, Func<T, T, bool> match = null)
: base (query, match)
{
notifier = collection as INotifyCollectionChanged;
IsObserving = true;
}
bool monitor = false;
public bool IsObserving
{
get { return monitor; }
set {
if (monitor == value)
return;
monitor = value;
if (notifier == null)
return;
if (monitor) {
notifier.CollectionChanged += HandleChange;
} else {
notifier.CollectionChanged -= HandleChange;
}
}
}
void HandleChange (object sender, NotifyCollectionChangedEventArgs e)
{
ThrottledRefresh ();
}
}
}
@praeclarum
Copy link
Author

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