[WPF] Delay the Execution of a method

Delaying the execution of code in WPF applications? Buy a slower computer or use this code.

Today, something almost embarrassing happened to me.

In one of my WPF projects I wanted a method to execute after a specified delay.

I played around with the Dispatcher class and soon discovered the method Dispatcher.Invoke(DispatcherPriority, TimeSpan, Delegate). I neither looked into the Doku nor at the IntelliSense popup (I don’t know where my mind was at that time 😀 ) and used the method.

Then I really desperated debugging that code. The delegate was always invoked at once.

Why?

Well – I should have read through the Doku. The TimeSpan parameter is no delay, but “The maximum time to wait for the operation to finish”.

Solving the “problem”

Now I had to write a custom class providing the possibility to delay the execution of a delegate.

Here it is:

/// <summary>
/// Provides functionality for delaying the execution of a <see cref="Delegate"/>.
/// </summary>
/// <remarks>This class will execute the <see cref="Delegate"/>s in the Thread associated with its <see cref="Dispatcher"/>.</remarks>
public sealed class DelayExecuter
{
    private readonly Timer timer;
    private readonly SortedList<int, Job> jobs;
    private readonly ResourceLock resourceLock;
    [ContractPublicPropertyName("Dispatcher")]
    private readonly Dispatcher dispatcher;

    /// <summary>
    /// Gets a value indicating whether this <see cref="DelayExecuter"/> has pending jobs.
    /// </summary>
    /// <value><c>true</c> if this instance has pending jobs; otherwise, <c>false</c>.</value>
    public bool HasJobs
    {
        get
        {
            using (this.resourceLock.WaitToRead())
            {
                return this.jobs.Count != 0;
            }
        }
    }
    /// <summary>
    /// Gets the dispatcher for this <see cref="DelayExecuter"/>.
    /// </summary>
    /// <value>The dispatcher.</value>
    public Dispatcher Dispatcher
    {
        get
        {
            Contract.Ensures(Contract.Result<Dispatcher>() != null);

            return this.dispatcher;
        }
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="DelayExecuter"/> class.
    /// </summary>
    /// <remarks><see cref="Dispatcher"/> is set to <see cref="System.Windows.Threading.Dispatcher.CurrentDispatcher"/></remarks>
    public DelayExecuter()
        : this(Dispatcher.CurrentDispatcher)
    {
        Contract.Ensures(this.Dispatcher != null);
        Contract.Ensures(Object.Equals(this.Dispatcher, Dispatcher.CurrentDispatcher));
        Contract.Ensures(!this.HasJobs);
    }
    /// <summary>
    /// Initializes a new instance of the <see cref="DelayExecuter"/> class.
    /// </summary>
    /// <param name="dispatcher">The dispatcher to use as <see cref="Dispatcher"/>.</param>
    public DelayExecuter(Dispatcher dispatcher)
    {
        Contract.Requires<ArgumentNullException>(dispatcher != null);
        Contract.Ensures(Object.Equals(this.Dispatcher, dispatcher));
        Contract.Ensures(!this.HasJobs);

        this.jobs = new SortedList<int, Job>();
        this.resourceLock = new OneManySpinResourceLock();

        this.dispatcher = dispatcher;

        this.timer = new Timer();
        this.timer.AutoReset = false;
        this.timer.Elapsed += this.timer_Elapsed;
    }

    /// <summary>
    /// Executes the specified <see cref="Delegate"/> after the specified delay.
    /// </summary>
    /// <param name="delay">The delay to wait</param>
    /// <param name="callback">The callback to execute.</param>
    /// <param name="args">The optional args for the <paramref name="callback"/>.</param>
    public void Execute(TimeSpan delay, Delegate callback, params object[] args)
    {
        Contract.Requires<ArgumentNullException>(callback != null);
        Contract.Requires<ArgumentException>(delay.TotalMilliseconds > 0);
        Contract.Requires<ArgumentException>(delay.TotalMilliseconds < int.MaxValue);
        Contract.Requires(callback.GetType().GetMethod("Invoke", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public).GetParameters().Length == args.Length);

        this.Execute(delay, DispatcherPriority.Normal, callback, args);
    }
    /// <summary>
    /// Executes the specified <see cref="Delegate"/> after the specified delay.
    /// </summary>
    /// <param name="delay">The delay to wait</param>
    /// <param name="priority">The <see cref="DispatcherPriority"/> to use to invoke the <paramref name="callback"/>.</param>
    /// <param name="callback">The callback to execute.</param>
    /// <param name="args">The optional args for the <paramref name="callback"/>.</param>
    public void Execute(TimeSpan delay, DispatcherPriority priority, Delegate callback, params object[] args)
    {
        Contract.Requires<ArgumentNullException>(callback != null);
        Contract.Requires<ArgumentException>(delay.TotalMilliseconds > 0);
        Contract.Requires<ArgumentException>(delay.TotalMilliseconds < int.MaxValue);
        Contract.Requires<InvalidEnumArgumentException>(Enum.IsDefined(typeof(DispatcherPriority), priority) && priority != DispatcherPriority.Invalid);
        Contract.Requires(callback.GetType().GetMethod("Invoke", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public).GetParameters().Length == args.Length);

        Job job = new Job(callback, args, priority);

        using (this.resourceLock.WaitToWrite())
        {
            this.timer.Stop();

            int executionTime = Environment.TickCount;
            unchecked
            {
                executionTime += (int) delay.TotalMilliseconds;
            }

            this.jobs.Add(executionTime, job);

            this.RefreshTimer();
        }
    }
    /// <summary>
    /// Executes the specified <see cref="Delegate"/> after the specified time.
    /// </summary>
    /// <param name="executionTime">The time when to execute the <paramref name="callback"/>.</param>
    /// <param name="callback">The callback to execute.</param>
    /// <param name="args">The optional args for the <paramref name="callback"/>.</param>
    public void Execute(DateTime executionTime, Delegate callback, params object[] args)
    {
        Contract.Requires<ArgumentNullException>(callback != null);
        Contract.Requires<ArgumentException>(executionTime.Subtract(DateTime.Now).TotalMilliseconds > 0);
        Contract.Requires<ArgumentException>(executionTime.Subtract(DateTime.Now).TotalMilliseconds < int.MaxValue);
        Contract.Requires(callback.GetType().GetMethod("Invoke", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public).GetParameters().Length == args.Length);

        this.Execute(executionTime, DispatcherPriority.Normal, callback, args);
    }
    /// <summary>
    /// Executes the specified <see cref="Delegate"/> after the specified time.
    /// </summary>
    /// <param name="executionTime">The time when to execute the <paramref name="callback"/>.</param>
    /// <param name="priority">The <see cref="DispatcherPriority"/> to use to invoke the <paramref name="callback"/>.</param>
    /// <param name="callback">The callback to execute.</param>
    /// <param name="args">The optional args for the <paramref name="callback"/>.</param>
    public void Execute(DateTime executionTime, DispatcherPriority priority, Delegate callback, params object[] args)
    {
        Contract.Requires<ArgumentNullException>(callback != null);
        Contract.Requires<ArgumentException>(executionTime.Subtract(DateTime.Now).TotalMilliseconds > 0);
        Contract.Requires<ArgumentException>(executionTime.Subtract(DateTime.Now).TotalMilliseconds < int.MaxValue);
        Contract.Requires<InvalidEnumArgumentException>(Enum.IsDefined(typeof(DispatcherPriority), priority) && priority != DispatcherPriority.Invalid);
        Contract.Requires(callback.GetType().GetMethod("Invoke", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public).GetParameters().Length == args.Length);

        this.Execute(executionTime.Subtract(DateTime.Now), priority, callback, args);
    }

    private void timer_Elapsed(object sender, EventArgs e)
    {
        using (this.resourceLock.WaitToWrite())
        {
            //in ms - cycels between int.MinValue and int.MaxValue
            int tickCount = Environment.TickCount;
            int right = tickCount;
            int left = unchecked(tickCount - int.MaxValue);

            int min = Math.Min(right, left);
            int max = Math.Max(right, left);

            while (this.jobs.Count != 0
                   && max >= this.jobs.Keys[0]
                   && min <= this.jobs.Keys[0])
            {
                Job job = this.jobs.Values[0];
                this.Dispatcher.BeginInvoke(job.Callback, job.Priority, job.Args);
                this.jobs.RemoveAt(0);
            }

            this.RefreshTimer();
        }
    }
    private void RefreshTimer()
    {
        Contract.Requires<InvalidOperationException>(!this.timer.Enabled);
        Contract.Ensures((this.jobs.Count != 0) == (this.timer.Enabled));

        if (this.jobs.Count == 0)
            return;

        int tickCount = Environment.TickCount;
        int timeToNext = Math.Abs(unchecked(jobs.Keys[0] - tickCount));

        if (timeToNext == 0)
            timeToNext = 1;

        this.timer.Interval = timeToNext;
        this.timer.Start();
    }

    [ContractInvariantMethod]
    private void DelayExecuterInvariant()
    {
        Contract.Invariant(this.resourceLock != null);
        Contract.Invariant(this.timer != null);
        Contract.Invariant(this.jobs != null);
        Contract.Invariant(this.Dispatcher != null);
        Contract.Invariant(ContractHelperExtensions.EvaluateLocking(() => (this.jobs.Count != 0) == (this.timer.Enabled), this.resourceLock));
    }
}

/// <summary>
/// Helper class for <see cref="DelayExecuter"/>.
/// </summary>
internal sealed class Job
{
    public Delegate Callback
    {
        get;
        private set;
    }
    public object[] Args
    {
        get;
        private set;
    }
    public DispatcherPriority Priority
    {
        get;
        private set;
    }

    public Job(Delegate callback, object[] args, DispatcherPriority priority)
    {
        Contract.Requires<ArgumentNullException>(callback != null);
        Contract.Requires<InvalidEnumArgumentException>(Enum.IsDefined(typeof(DispatcherPriority), priority) && priority != DispatcherPriority.Invalid);

        Contract.Ensures(this.Callback == callback);
        Contract.Ensures(this.Args == args);
        Contract.Ensures(this.Priority == priority);

        this.Callback = callback;
        this.Args = args;
        this.Priority = priority;
    }

    [ContractInvariantMethod]
    private void JobInvariant()
    {
        Contract.Invariant(this.Callback != null);
        Contract.Invariant(Enum.IsDefined(typeof(DispatcherPriority), this.Priority) && this.Priority != DispatcherPriority.Invalid);
    }
}

Note: ResourceLock is part of the Wintellect Power Threading Library. If you do not want to use it, you can simply change it to object an use lock instead of “using (resourceLock.WaitToXXX())”.

Point of Interest

My DelayExecuter uses a Timer to delay the execution. I decided to use a System.Timers.Timer instead of a WPF-specific DispatcherTimer and perform the invoking manually. The reason for that is a little “spark” of parallelism 🙂

Job encapsulates a job that will be executed at some further time.

Every time a new Job is added to the DelayExecuter or when the Timer elapses, its interval is set to the time at which the next Job should be executed. In order to have quick and easy access to the next Job that should be executed, I am using a SortedList.

Close

If you know any better solution or find any mistakes in mine – please let me know.

//EDIT:

tobi found some bugs – thanks for reporting them.

I updated the DelayExecuter so it uses UTC-time Environment.TickCount to be more stable when daylight saving time begins / end and the system time changes. Also, I removed dangerous Contract.Ensures.

DotNetKicks Image
Advertisements

7 Responses to “[WPF] Delay the Execution of a method”

  1. tobi Says:

    DateTime.Now might be a problem because the program will stop working the second daylight saving time starts. you might consider Environment.TickCount instead.

    Contract.Ensures(this.HasJobs);

    i don’t think you can guarantee a caller this property. if the timer callback executes between your method return and the caller observes HasJobs, it might be false. actually i don’t know if contracts make sense in a multithreaded situation.

  2. winsharp93 Says:

    >> Contract.Ensures(this.HasJobs);
    >> i don’t think you can guarantee a caller this property
    Yes, you are totally right.
    The background why I wrote this Ensures was, that I first used a DispatcherTimer. Then I replaced it with a System.Threading.Timer and forgot to remove the Ensures.
    Thanks!

    >> DateTime.Now might be a problem because the program will stop working the second daylight saving time starts
    Thanks again! I really forgot about that.
    I am now using UTC-Times – this should also solve the problem.

    Cheers
    winSharp93

  3. tobi Says:

    i am not so sure that utc is enough. is it ok that the program stops working when the user changes the system time? what if the pc is auto-syncing with an ntp server? time can fast-forward, freeze and go backwards on computers.

  4. winsharp93 Says:

    >> what if the pc is auto-syncing with an ntp server? time can fast-forward, freeze and go backwards on computers.
    Ok – you are right again.
    Thanks for busying yourself with my posting.

    I am now using Environment.TickCount – so I hope there are no bugs this time.
    It seems I should keep away from everything which has to do with date and time… 🙂

    Cheers
    winSharp93

  5. Kuba Says:

    What about:

    var dt = new DispatcherTimer();
    dt.Tick += (s, e) => {MyMethod(); dt.Stop();};
    dt.Interval = new TimeSpan();
    dt.Start();

  6. winsharp93 Says:

    No – I did not forget my blog 😉

    >> What about:
    Well – that’s the easiest possibility and definitely enough in most cases. However, the perfromance will suffer from the creation of that many timer objects – in my case I would have created about 20 Timers a second.

  7. munc Says:

    Why not using ThreadPool.UnsafeRegisterWaitForSingleObject with an event that is never signaled. That will invoke your method at the exact time and you will only need to call dispatcher.BeginInvoke(…) in the callback.

    This leaves the complexity of scheduling your tasks to the ThreadPool.


Comments are closed.

%d bloggers like this: