Created
September 10, 2019 07:43
-
-
Save jimmylewis/423b51a6029575a37b7b39797c62d389 to your computer and use it in GitHub Desktop.
BindableBaseWithThrottling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Runtime.CompilerServices; | |
namespace ThrottlingReview | |
{ | |
public abstract class BindableBase : INotifyPropertyChanged | |
{ | |
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "") | |
{ | |
if (EqualityComparer<T>.Default.Equals(storage, value)) | |
{ | |
return false; | |
} | |
storage = value; | |
NotifyPropertyChanged(propertyName); | |
return true; | |
} | |
#region INotifyPropertyChanged | |
protected virtual void NotifyPropertyChanged([CallerMemberName] string propName = "") | |
{ | |
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); | |
} | |
public event PropertyChangedEventHandler PropertyChanged; | |
#endregion | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Concurrent; | |
using System.Runtime.CompilerServices; | |
using System.Threading.Tasks; | |
namespace ThrottlingReview | |
{ | |
public abstract class BindableBaseWithThrottling : BindableBase | |
{ | |
private readonly TimeSpan _throttleLimit; | |
private readonly TaskScheduler _taskScheduler; | |
private readonly ConcurrentDictionary<string, Task> _cooldownTasks; | |
private readonly ConcurrentDictionary<string, string> _pendingNotifications; | |
public BindableBaseWithThrottling(TimeSpan throttleLimit, TaskScheduler taskScheduler) | |
{ | |
_throttleLimit = throttleLimit; | |
_taskScheduler = taskScheduler; | |
_cooldownTasks = new ConcurrentDictionary<string, Task>(); | |
_pendingNotifications = new ConcurrentDictionary<string, string>(); | |
} | |
protected override void NotifyPropertyChanged([CallerMemberName] string propName = "") | |
{ | |
if (_pendingNotifications.ContainsKey(propName)) | |
{ | |
// do nothing, it's already pending | |
} | |
else if (_cooldownTasks.TryGetValue(propName, out _)) | |
{ | |
_pendingNotifications.TryAdd(propName, propName); | |
} | |
else | |
{ | |
// fire right away if we're not already in a cool down | |
base.NotifyPropertyChanged(propName); | |
Cooldown(propName); | |
} | |
} | |
private void Cooldown(string propName) | |
{ | |
var newCooldown = new Task(async () => await Task.Delay(_throttleLimit)); | |
if (_cooldownTasks.TryAdd(propName, newCooldown)) | |
{ | |
newCooldown.Start(_taskScheduler); | |
newCooldown.ContinueWith((t) => CheckForPendingNotifications(propName), _taskScheduler); | |
} | |
} | |
private void CheckForPendingNotifications(string propName) | |
{ | |
_cooldownTasks.TryRemove(propName, out _); | |
// if there was a new pending request during the cooldown, service it now | |
if (_pendingNotifications.TryRemove(propName, out _)) | |
{ | |
base.NotifyPropertyChanged(propName); | |
Cooldown(propName); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Threading.Tasks; | |
using Xunit; | |
namespace ThrottlingReview | |
{ | |
public class BindableBaseWithThrottlingTests | |
{ | |
[Fact] | |
public void NotifyPropertyChanged_CalledOnce_FiresImmediately() | |
{ | |
int count = 0; | |
var scheduler = new DeterministicTaskScheduler(); | |
var sut = new TestBindableObject(TimeSpan.FromMilliseconds(1), scheduler); | |
sut.PropertyChanged += (s, e) => | |
{ | |
count++; | |
}; | |
sut.Value = 1; // trigger INPC notification | |
Assert.Equal(1, count); | |
} | |
[Fact] | |
public void NotifyPropertyChanged_CalledOnce_FiresOnce() | |
{ | |
int count = 0; | |
var scheduler = new DeterministicTaskScheduler(); | |
var sut = new TestBindableObject(TimeSpan.FromMilliseconds(1), scheduler); | |
sut.PropertyChanged += (s, e) => | |
{ | |
count++; | |
}; | |
sut.Value = 1; // trigger INPC notification | |
scheduler.RunTasksUntilIdle(); | |
Assert.Equal(1, count); | |
} | |
[Fact] | |
public void NotifyPropertyChanged_CalledTwiceImmediately_OnlyOneEventFiredImmediately() | |
{ | |
int count = 0; | |
var scheduler = new DeterministicTaskScheduler(); | |
var sut = new TestBindableObject(TimeSpan.FromMilliseconds(1), scheduler); | |
sut.PropertyChanged += (s, e) => | |
{ | |
count++; | |
}; | |
sut.Value = 1; // trigger INPC notification | |
sut.Value = 2; // trigger after cooldown, but taskScheduler has not run any tasks, still in cooldown | |
Assert.Equal(1, count); | |
} | |
[Fact] | |
public void NotifyPropertyChanged_CalledTwiceImmediately_FiresSecondWithDelay() | |
{ | |
int count = 0; | |
var scheduler = new DeterministicTaskScheduler(); | |
var sut = new TestBindableObject(TimeSpan.FromMilliseconds(1), scheduler); | |
sut.PropertyChanged += (s, e) => | |
{ | |
count++; | |
}; | |
sut.Value = 1; // trigger INPC notification | |
sut.Value = 2; // trigger INPC notification after cooldown | |
scheduler.RunTasksUntilIdle(); | |
Assert.Equal(2, count); | |
} | |
[Fact] | |
public void NotifyPropertyChanged_CalledThriceImmediately_FiresSecondWithDelay() | |
{ | |
int count = 0; | |
var scheduler = new DeterministicTaskScheduler(); | |
var sut = new TestBindableObject(TimeSpan.FromMilliseconds(1), scheduler); | |
sut.PropertyChanged += (s, e) => | |
{ | |
count++; | |
}; | |
sut.Value = 1; // trigger INPC notification | |
sut.Value = 2; // trigger INPC notification after cooldown | |
sut.Value = 3; // falls within 2nd notification | |
scheduler.RunTasksUntilIdle(); | |
Assert.Equal(2, count); | |
} | |
[Fact] | |
public void NotifyPropertyChanged_CalledTwiceWithDelay_FiresSecondImmediately() | |
{ | |
int count = 0; | |
var scheduler = new DeterministicTaskScheduler(); | |
var sut = new TestBindableObject(TimeSpan.FromMilliseconds(1), scheduler); | |
sut.PropertyChanged += (s, e) => | |
{ | |
count++; | |
}; | |
sut.Value = 1; // trigger INPC notification | |
scheduler.RunTasksUntilIdle(); | |
sut.Value = 2; // trigger INPC notification after cooldown | |
Assert.Equal(2, count); | |
} | |
[Fact] | |
public void NotifyPropertyChanged_CalledWithDifferentPropertyNames_FiresImmediatelyForEach() | |
{ | |
int count = 0; | |
var scheduler = new DeterministicTaskScheduler(); | |
var sut = new TestBindableObject(TimeSpan.FromMilliseconds(1), scheduler); | |
sut.PropertyChanged += (s, e) => | |
{ | |
count++; | |
}; | |
sut.Value = 1; // trigger INPC notification | |
sut.SecondValue = "test"; // trigger INPC notification for 2nd property name | |
Assert.Equal(2, count); | |
} | |
private class TestBindableObject : BindableBaseWithThrottling | |
{ | |
private int _value; | |
private string _secondValue; | |
public TestBindableObject(TimeSpan throttleLimit, TaskScheduler taskScheduler) | |
: base(throttleLimit, taskScheduler) | |
{ | |
} | |
public int Value | |
{ | |
get { return _value; } | |
set { SetProperty(ref _value, value); } | |
} | |
public string SecondValue | |
{ | |
get { return _secondValue; } | |
set { SetProperty(ref _secondValue, value); } | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
namespace ThrottlingReview | |
{ | |
/// <summary> | |
/// TaskScheduker for executing tasks on the same thread that calls RunTasksUntilIdle() or RunPendingTasks() | |
/// </summary> | |
public class DeterministicTaskScheduler : TaskScheduler | |
{ | |
private List<Task> _scheduledTasks = new List<Task>(); | |
#region TaskScheduler methods | |
protected override void QueueTask(Task task) | |
{ | |
_scheduledTasks.Add(task); | |
} | |
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) | |
{ | |
_scheduledTasks.Add(task); | |
return false; | |
} | |
protected override IEnumerable<Task> GetScheduledTasks() | |
{ | |
return _scheduledTasks; | |
} | |
public override int MaximumConcurrencyLevel { get { return 1; } } | |
#endregion | |
/// <summary> | |
/// Executes the scheduled Tasks synchronously on the current thread. If those tasks schedule new tasks | |
/// they will also be executed until no pending tasks are left. | |
/// </summary> | |
public void RunTasksUntilIdle() | |
{ | |
while (_scheduledTasks.Any()) | |
{ | |
RunPendingTasks(); | |
} | |
} | |
/// <summary> | |
/// Executes the scheduled Tasks synchronously on the current thread. If those tasks schedule new tasks | |
/// they will only be executed with the next call to RunTasksUntilIdle() or RunPendingTasks(). | |
/// </summary> | |
public void RunPendingTasks() | |
{ | |
foreach (Task task in _scheduledTasks.ToArray()) | |
{ | |
TryExecuteTask(task); | |
_scheduledTasks.Remove(task); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment