views:

133

answers:

3

I'm using following code to call Method B after N seconds method A is called. If method A is called again within the N seconds timeout, i have to reset the time counting back to N seconds.

I cannot reference System.Windows.Form in my project, so I cannot use System.Windows.Form.Timer.

The method B must be called in the same thread A is called.

private void InitTimer()
{
    timer = new BackgroundWorker();
    timer.WorkerSupportsCancellation = true;
    timer.WorkerReportsProgress = true;
    timer.DoWork += delegate(object sender, DoWorkEventArgs e)
                    {
                        var st = DateTime.Now;
                        while (DateTime.Now.Subtract(st).TotalSeconds < 10)
                        {
                            if (timer.CancellationPending)
                            {
                                e.Cancel = true;
                                return;
                            }
                        }

                    };
    timer.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs e)
                    {
                        if (!e.Cancelled)
                        {    
                            MethodB();
                        }
                        else
                        {
                            timer.RunWorkerAsync();
                        }
                    };
}



public void MethodA()
{
     if (timer.IsBusy)
         timer.CancelAsync();
     else
         timer.RunWorkerAsync();

}

public void MethodB()
{
     //do some stuff

}

Actually the code work, but i think it's a bit confounding. Do you know if there is a best practices to achieve the same result?

+1  A: 

I would encapsulate this functionality into a timer class with events that other classes can subscribe to (for example a timer.tick event).

Adrian Grigore
I thought of it. In such way I can clarify the intent of timer.CancelAsync() and timer.RunWorkerAsync(). Do you think there some contraindication or side effect in my code?
Andrea Parodi
+4  A: 

It's a shame you're stuck on .NET 2.0, because Rx extensions has a Throttle method that achieves this effect quite elegantly.

Sadly Rx requires at least .NET 3.5 SP1.

Oh well! You can always use a System.Threading.Timer to get this done instead. Synchronization can be provided by leveraging the current SynchronizationContext (this is what BackgroundWorker does).

Here's a sketch of a LaggedMethodPair class to illustrate this approach. The class takes three inputs in its constructor: an Action to be performed on-demand, another Action to serve as the callback that will be invoked when a given timeout has elapsed, and, of course, the timeout itself:

public sealed class LaggedMethodPair
{
    private SynchronizationContext _context;
    private Timer _timer;

    private Action _primaryAction;
    private Action _laggedCallback;
    private int _millisecondsLag;

    public LaggedMethodPair(Action primaryAction,
                            Action laggedCallback,
                            int millisecondsLag)
    {
        if (millisecondsLag < 0)
        {
            throw new ArgumentOutOfRangeException("Lag cannot be negative.");
        }

        // Do nothing by default.
        _primaryAction = primaryAction ?? new Action(() => { });

        // Do nothing by default.
        _laggedCallback = laggedCallback ?? new Action(() => { });

        _millisecondsLag = millisecondsLag;

        _timer = new Timer(state => RunTimer());
    }

    public void Invoke()
    {
        // Technically there is a race condition here.
        // It could be addressed, but in practice it will
        // generally not matter as long as Invoke is always
        // being called from the same SynchronizationContext.
        if (SynchronizationContext.Current == null)
        {
            SynchronizationContext.SetSynchronizationContext(
                new SynchronizationContext()
            );
        }

        _context = SynchronizationContext.Current;

        ResetTimer();

        _primaryAction();
    }

    void ResetTimer()
    {
        _timer.Change(_millisecondsLag, Timeout.Infinite);
    }

    void RunTimer()
    {
        _context.Post(state => _laggedCallback(), null);
    }
}

I wrote a sample Windows Forms app to show this class in action. The form contains a LaggedMethodPair member with a timeout of 2000 ms. Its primaryAction adds an item to a list view. Its laggedCallback adds a highlighted item to the list view.

You can see that the code runs as expected.

LaggedMethodPair Test Form

Dan Tao
Ok, but in this way TimerCallback is not execute in its own thread? I have to call MethodB() in the same thread I call MethodA()
Andrea Parodi
Yes, you're right: `MethodB` would be invoked on a threadpool thread. Typically if it needs to execute from the same thread as `MethodA`, you would either (a) use your own synchronization mechanism (e.g., in a WinForms app, you might use `Control.BeginInvoke`); or (b) take advantage of some built-in synchronization technique, such as attaching to either an `ISynchronizeInvoke` object (this is what the `System.Timers.Timer` class does) or a `SynchronizationContext` (this is what the `BackgroundWorker` class does). I will update the answer to provide an example.
Dan Tao
Thank you Dan, very useful and clear.
Andrea Parodi
A: 

I am trying to use AutoResetEvent, because it is capable to wait for a signal. I use it to have worker waited for the signal from A(), and if it has been too long B() will be called.

class Caller
{
    AutoResetEvent ev = new AutoResetEvent(false);
    public void A()
    {
        ev.Set();
        // do your stuff
        Console.Out.WriteLine("A---");
    }
    void B()
    {
        Console.Out.WriteLine("B---");
    }

    public void Start()
    {
        var checker = new BackgroundWorker();
        checker.DoWork += new DoWorkEventHandler(checker_DoWork);
        checker.RunWorkerAsync();
    }

    void checker_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
        while (!worker.CancellationPending)
        {
            bool called = ev.WaitOne(TimeSpan.FromSeconds(3));
            if (!called) B();
        }
    }
}

I have tested my class roughly and it is working fine so far. Note that B will be called from worker thread, so you have to do the synchronization in B() if needed.

tia