Notatnik techniczny

Async command #2

Async command z opóźnieniem

Kolejny kawałek kodu odpowiedzialny za kontrolowanie wywołania innej funkcji. Przykład zastosowania klasy DelayedSingleCommand:

  • w odtwarzaczu video ukrywamy kursor myszki 5 sekund po ostatnim poruszeniu myszką,
  • przetwarzamy ponownie dany plik, jeśli uległ modyfikacji, ale przez ostatnie 100 sekund już nie był modyfikowany ponownie.

Opis działania klasy:

  • w chwili pojawienia się wywołania rozpoczynamy odliczanie czasu Δ,
  • jeśli w czasie oczekiwania Δ pojawiło się kolejne wywołanie, resetujemy licznik czasu,
  • jeśli czas Δ upłynie, wywołujemy wówczas kontrolowaną funkcję,
  • kontrolowana funkcja będzie wywołana w kontekście wątku tworzącego obiekt lub w wątku timera zależnie od parametru przekazanego w konstruktorze,
  • w przypadku wyrzucenia wyjątku w kontrolowanej funkcji, przekazujemy go do funkcji obsługi wyjątku, jeśli jest zdefiniowana.

Implementacja

using System;
using System.Threading;
using Timer = System.Threading.Timer;
 
namespace Demo.Commands
{
    public class DelayedSingleCommand
    {
        public DelayedSingleCommand(
            Action task, 
            Action<Exception> errorTask, 
            int delay,  
            bool useThisContex = true )
        {
            if (useThisContex)
                _contex = SynchronizationContext.Current;
            _task = task;
            _errorTask = errorTask;
            _delay = delay;
            if (_task == null) throw new ArgumentNullException();
            _timer = new Timer(TimerCb, 
                               null, 
                               Timeout.Infinite, 
                               Timeout.Infinite);
        }
 
        public void Execute()
        {
            _timer.Change(_delay, Timeout.Infinite);
        }
 
        public void Cancel()
        {
            _timer.Change(Timeout.Infinite, Timeout.Infinite);
        }
 
        //=======================================
 
        private readonly SynchronizationContext _contex;
        private readonly Action _task;
        private readonly Action<Exception> _errorTask;
        private readonly int _delay;
        private readonly Timer _timer;
 
        private void InvokeTask(Action task)
        {
            if (_contex == null)
            {
                task.Invoke();
                return;
            }
            _contex.Send( b => task.Invoke(), null);
        }
 
        private void TimerCb(object state)
        {
            try
            {
                InvokeTask(_task);
            }
            catch(Exception e)
            {
                if (_errorTask != null)
                    InvokeTask(() => _errorTask.Invoke(e));
            }
        }
    }
}

Kilka testów

using System;
using System.Threading;
using Demo.Commands;
using NUnit.Framework;
 
namespace Demo.Tests
{
    [TestFixture]
    public class DelayedSingleCommandTest
    {
        [Test]
        public void Test1()
        {
            var state = 0;
            var obj = new DelayedSingleCommand(() => ++state, null, 100, true);
            obj.Execute();
            Assert.AreEqual(0, state);
            Thread.Sleep(200);
            Assert.AreEqual(1, state);
        }
 
        [Test]
        public void Test2()
        {
            var state = 0;
            var obj = new DelayedSingleCommand(() => ++state, null, 100, false);
            obj.Execute();
            Assert.AreEqual(0, state);
            Thread.Sleep(200);
            Assert.AreEqual(1, state);
        }
 
        [Test]
        public void Test3()
        {
            var state = 0;
            var obj = new DelayedSingleCommand(() => ++state, null, 100, true);
            obj.Execute();
            obj.Execute();
            obj.Execute();
            obj.Execute();
            Assert.AreEqual(0, state);
            Thread.Sleep(80);
            Assert.AreEqual(0, state);
            obj.Execute();
            Thread.Sleep(80);
            Assert.AreEqual(0, state);
            Thread.Sleep(50);
            Assert.AreEqual(1, state);
        }
 
        [Test]
        public void Test4()
        {
            var state = 0;
            var obj = new DelayedSingleCommand(() => ++state, null, 100, false);
            obj.Execute();
            obj.Execute();
            obj.Execute();
            obj.Execute();
            Assert.AreEqual(0, state);
            Thread.Sleep(80);
            Assert.AreEqual(0, state);
            obj.Execute();
            Thread.Sleep(80);
            Assert.AreEqual(0, state);
            Thread.Sleep(50);
            Assert.AreEqual(1, state);
        }
 
        [Test]
        public void Test5()
        {
            var state = 0;
            var obj = new DelayedSingleCommand(() => ++state, null, 100, true);
            obj.Execute();
            obj.Execute();
            obj.Execute();
            obj.Execute();
            Assert.AreEqual(0, state);
            Thread.Sleep(80);
            obj.Cancel();
            Assert.AreEqual(0, state);
            Thread.Sleep(80);
            Assert.AreEqual(0, state);
        }
 
        [Test]
        public void TestError1()
        {
            bool errorProcessed = false;
            var tesk = new Action(() => { throw new ApplicationException(); } );
            var errorTask = new Action<Exception>(e => { errorProcessed = true; ; });
            var obj = new DelayedSingleCommand(tesk, errorTask, 50, true);
            obj.Execute();
            Thread.Sleep(100);
            Assert.AreEqual(errorProcessed, true);
        }
 
        [Test]
        public void TestError2()
        {
            bool errorProcessed = false;
            var tesk = new Action(() => { throw new ApplicationException(); });
            var errorTask = new Action<Exception>(e => { errorProcessed = true; });
            var obj = new DelayedSingleCommand(tesk, errorTask, 50, false);
            obj.Execute();
            Thread.Sleep(100);
            Assert.AreEqual(errorProcessed, true);
        }
 
    }
}

Odnośniki