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 @@
+