diff --git a/source/Zyan.Communication/Toolbox/Debouncer.cs b/source/Zyan.Communication/Toolbox/Debouncer.cs new file mode 100644 index 00000000..d0804586 --- /dev/null +++ b/source/Zyan.Communication/Toolbox/Debouncer.cs @@ -0,0 +1,92 @@ +using System; +using SysTimer = System.Timers.Timer; + +namespace Zyan.Communication.Toolbox +{ + internal static class Debouncer + { + /// + /// Default debounce interval, milliseconds. + /// + public const int DefaultDebounceInterval = 300; + + /// + /// Creates a new debounced version of the passed action. + /// + /// Action to debounce. + /// Debounce interval in milliseconds. + /// The debounced version of the given action. + /// + /// Based on these gists: + /// https://gist.github.com/ca0v/73a31f57b397606c9813472f7493a940 + /// https://gist.github.com/fr-ser/ded7690b245223094cd876069456ed6c + /// + public static Action Debounce(this Action action, int delayMs = DefaultDebounceInterval) + { + var timer = default(IDisposable); + + return () => + { + timer?.Dispose(); + timer = SetTimeout(action, delayMs); + }; + } + + /// + /// Executes an action at specified intervals (in milliseconds), like setInterval in Javascript. + /// + /// The action to schedule. + /// The delay in milliseconds. + /// + /// The value that can be disposed to stop the timer. + /// + /// + /// Based on this gist: + /// https://gist.github.com/CipherLab/10a40f7032be04f0aa6f + /// + public static IDisposable SetInterval(Action action, int delayMs) + { + var timer = new SysTimer(delayMs) + { + AutoReset = true + }; + + timer.Elapsed += (s, e) => + { + action(); + }; + + timer.Start(); + return timer; + } + + /// + /// Executes an action after a specified number of milliseconds. + /// + /// The action to execute. + /// The delay in milliseconds. + /// + /// The value that can be disposed to cancel the execution. + /// + /// + /// Based on this gist: + /// https://gist.github.com/CipherLab/10a40f7032be04f0aa6f + /// + public static IDisposable SetTimeout(Action action, int delayMs) + { + var timer = new SysTimer(delayMs) + { + AutoReset = false + }; + + timer.Elapsed += (s, e) => + { + action(); + timer.Dispose(); + }; + + timer.Start(); + return timer; + } + } +} diff --git a/source/Zyan.Communication/Zyan.Communication.Android.csproj b/source/Zyan.Communication/Zyan.Communication.Android.csproj index 08833ca3..8f14fecf 100644 --- a/source/Zyan.Communication/Zyan.Communication.Android.csproj +++ b/source/Zyan.Communication/Zyan.Communication.Android.csproj @@ -263,6 +263,7 @@ + diff --git a/source/Zyan.Communication/Zyan.Communication.Fx3.csproj b/source/Zyan.Communication/Zyan.Communication.Fx3.csproj index ee092c20..f6dbff3b 100644 --- a/source/Zyan.Communication/Zyan.Communication.Fx3.csproj +++ b/source/Zyan.Communication/Zyan.Communication.Fx3.csproj @@ -307,6 +307,7 @@ + diff --git a/source/Zyan.Communication/Zyan.Communication.Fx4.csproj b/source/Zyan.Communication/Zyan.Communication.Fx4.csproj index 965c65b3..67827ad1 100644 --- a/source/Zyan.Communication/Zyan.Communication.Fx4.csproj +++ b/source/Zyan.Communication/Zyan.Communication.Fx4.csproj @@ -294,6 +294,7 @@ + diff --git a/source/Zyan.Communication/Zyan.Communication.Fx45.csproj b/source/Zyan.Communication/Zyan.Communication.Fx45.csproj index 2a5c8d02..fbfdda59 100644 --- a/source/Zyan.Communication/Zyan.Communication.Fx45.csproj +++ b/source/Zyan.Communication/Zyan.Communication.Fx45.csproj @@ -314,6 +314,7 @@ + diff --git a/source/Zyan.Communication/Zyan.Communication.Mono.csproj b/source/Zyan.Communication/Zyan.Communication.Mono.csproj index 02af3d1b..9cc31540 100644 --- a/source/Zyan.Communication/Zyan.Communication.Mono.csproj +++ b/source/Zyan.Communication/Zyan.Communication.Mono.csproj @@ -319,6 +319,7 @@ + diff --git a/source/Zyan.Communication/Zyan.Communication.csproj b/source/Zyan.Communication/Zyan.Communication.csproj index 54f3e3c6..5cb911e1 100644 --- a/source/Zyan.Communication/Zyan.Communication.csproj +++ b/source/Zyan.Communication/Zyan.Communication.csproj @@ -312,6 +312,7 @@ + diff --git a/source/Zyan.Tests/DebouncerTests.cs b/source/Zyan.Tests/DebouncerTests.cs new file mode 100644 index 00000000..1f9605e8 --- /dev/null +++ b/source/Zyan.Tests/DebouncerTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Zyan.Communication.Toolbox; + +namespace Zyan.Tests +{ + #region Unit testing platform abstraction layer +#if NUNIT + using NUnit.Framework; + using TestClass = NUnit.Framework.TestFixtureAttribute; + using TestMethod = NUnit.Framework.TestAttribute; + using ClassInitializeNonStatic = NUnit.Framework.TestFixtureSetUpAttribute; + using ClassInitialize = DummyAttribute; + using ClassCleanupNonStatic = NUnit.Framework.TestFixtureTearDownAttribute; + using ClassCleanup = DummyAttribute; + using TestContext = System.Object; +#else + using Microsoft.VisualStudio.TestTools.UnitTesting; + using ClassCleanupNonStatic = DummyAttribute; + using ClassInitializeNonStatic = DummyAttribute; +#endif + #endregion + + /// + /// Test class for the debouncer. + /// + [TestClass] + public class DebouncerTests + { + [TestMethod] + public void SetTimeoutExecutesTheGivenActionAfterAnInterval() + { + // target function + var counter = 0; + Action inc = () => counter++; + + Debouncer.SetTimeout(inc, 10); + Assert.AreEqual(0, counter); + + Thread.Sleep(50); + Assert.AreEqual(1, counter); + } + + [TestMethod] + public void SetTimeoutCanBeCancelled() + { + // target function + var counter = 0; + Action inc = () => counter++; + + var timer = Debouncer.SetTimeout(inc, 10); + Assert.AreEqual(0, counter); + timer.Dispose(); + + Thread.Sleep(50); + Assert.AreEqual(0, counter); + } + + [TestMethod] + public void SetIntervalExecutesTheGivenActionAtAGivenInterval() + { + // target function + var counter = 0; + Action inc = () => counter++; + + Debouncer.SetInterval(inc, 10); + Assert.AreEqual(0, counter); + + Thread.Sleep(50); + Assert.IsTrue(counter > 1); + } + + [TestMethod] + public void SetIntervalCanBeCancelled() + { + // target function + var counter = 0; + Action inc = () => counter++; + + var timer = Debouncer.SetInterval(inc, 10); + Assert.AreEqual(0, counter); + + Thread.Sleep(50); + Assert.IsTrue(counter > 1); + timer.Dispose(); + + var lastCounter = counter; + Thread.Sleep(50); + Assert.AreEqual(lastCounter, counter); + } + + [TestMethod] + public void DebouncedActionIsCalledOnce() + { + // target function + var counter = 0; + Action inc = () => counter++; + + // debounce the given function + var debounced = inc.Debounce(10); + Assert.IsNotNull(debounced); + + // try to call the debounced version and make sure the target is not yet called + debounced(); + debounced(); + debounced(); + debounced(); + Assert.AreEqual(0, counter); + + Thread.Sleep(50); + Assert.AreEqual(1, counter); + } + } +} diff --git a/source/Zyan.Tests/Zyan.Tests.Fx3.csproj b/source/Zyan.Tests/Zyan.Tests.Fx3.csproj index 768edeb1..2460dea5 100644 --- a/source/Zyan.Tests/Zyan.Tests.Fx3.csproj +++ b/source/Zyan.Tests/Zyan.Tests.Fx3.csproj @@ -80,6 +80,7 @@ + diff --git a/source/Zyan.Tests/Zyan.Tests.Mono.csproj b/source/Zyan.Tests/Zyan.Tests.Mono.csproj index a5bbce91..4322c1a8 100644 --- a/source/Zyan.Tests/Zyan.Tests.Mono.csproj +++ b/source/Zyan.Tests/Zyan.Tests.Mono.csproj @@ -64,6 +64,7 @@ + diff --git a/source/Zyan.Tests/Zyan.Tests.NUnit.csproj b/source/Zyan.Tests/Zyan.Tests.NUnit.csproj index de0c837c..7a470132 100644 --- a/source/Zyan.Tests/Zyan.Tests.NUnit.csproj +++ b/source/Zyan.Tests/Zyan.Tests.NUnit.csproj @@ -72,6 +72,7 @@ + diff --git a/source/Zyan.Tests/Zyan.Tests.csproj b/source/Zyan.Tests/Zyan.Tests.csproj index 6d7521b3..0d55d008 100644 --- a/source/Zyan.Tests/Zyan.Tests.csproj +++ b/source/Zyan.Tests/Zyan.Tests.csproj @@ -87,6 +87,7 @@ +