Geeks With Blogs

News

View Anthony Trudeau's profile on LinkedIn

Add to Technorati Favorites


Anthony Trudeau

You may have a need to perform an action way off in the future based on a specific date and time.  The future date could be a day, week, month, etc.  The timers provided within the .NET Framework do not support this scenario; however, they do provide the core mechanism.

The first step is choosing a timer.  There is the System.Timers.Timer, System.Threading.Timer, and of course the System.Windows.Forms timer which is designed for Windows Forms applications.  I chose the System.Threading.Timer, because it's lightweight and uses the semantics that will work best for my calendar-based timer.  The System.Timers.Timer is fine, but uses Start/Stop semantics whereas the System.Threading.Timer runs with a simple if not verbose Change method.  The real advantage is that I can set it to execute based on a due time (System.TimeSpan) and specify no period so that it executes only once.  At that point, I can specify a new due time or be done.

At this point you may be thinking, where's the need for a separate class?  I could just specify a TimeSpan equal to the difference between when I want to perform the action and the current time.  Unfortunately, that could be problematic because of pesky things like daylight savings time, leap years, or even clock issues.  The best solution is to use a combination of the TimeSpan and a real check of the current date and time.  I implemented this using a graduated scale going from less frequent to more frequent.  When the time between now and the run date/time is long I use a larger TimeSpan; and when its closer I use a successively smaller TimeSpan until I hit my target date/time.

I'm using composition instead of inheritance.  And overall the implementation is very simple.  The members of my class are as follows:

  • (event) TimerElapsed
  • ElapseDate { get; }
  • Started { get; }
  • Start(DateTime)
  • Stop()

The TimerElapsed event is raised when the ElapseDate is reached and is called via the callback method assigned to the internal timer (System.Threading.Timer).  It's only raised when that date/time is reached and not everytime the callback is called.  The callback will be called whenever the date/time needs to be checked.

The ElapseDate property is set when the Start method is called.  It's exposed for the use of the client application.  That is also the case for the Started property.

The Start method initiates the waiting and initializes the System.Threading.Timer the first time it's called.  After that it's a simple call to the internal timer's Change method.  The Stop method also calls the internal timer's Change method with special values to disable it.

public void Start(DateTime elapseDate)

{

    if (disposed)

        throw new ObjectDisposedException(this.GetType().Name);

 

    TimeSpan dueTime = CalculateDueTime(elapseDate);

    ElapseDate = elapseDate;

    Started = true;

 

    if (timer == null)

    {

        TimerCallback callback = new TimerCallback(OnCheckDateTime);

        timer = new Timer(callback, null, dueTime, noPeriod);

    }

    else

    {

        timer.Change(dueTime, noPeriod);

    }

}

public void Stop()

{

    if (disposed)

        throw new ObjectDisposedException(this.GetType().Name);

 

    Started = false;

    timer.Change(-1, 0);    // stops the timer

}

 

The OnCheckDateTime callback method is where most of the work is done.  It checks out how much time is remaining and calls a helper method to determine when the internal timer should next check, or if the ElapseDate is now than it raises the TimerElapsed event.  Simple and clean.

private void OnCheckDateTime(object state)

{

    TimeSpan timeDelta = ElapseDate.Subtract(DateTime.Now);

 

    if (timeDelta.TotalSeconds <= 0.0)

    {

        // the elapse date/time has been reached

        Stop();

        OnTimerElapsed(this, EventArgs.Empty);

    }

    else

    {

        // refine the period for the internal timer

        TimeSpan newDueTime = CalculateDueTime(timeDelta);

        timer.Change(newDueTime, noPeriod);

    }

}

 

The helper method that determines the due time (a TimeSpan object) is not so clean.  It calculates the time that is left and determines the new due time for the internal timer using a series of if and else statements.

private static TimeSpan CalculateDueTime(DateTime elapseDate)

{

    TimeSpan timeDelta = elapseDate.Subtract(DateTime.Now);

    return CalculateDueTime(timeDelta);

}

private static TimeSpan CalculateDueTime(TimeSpan nextRun)

{

    double minutes = nextRun.TotalMinutes;

    double hours = nextRun.TotalHours;

 

    if (hours > 24)

        return new TimeSpan(24, 0, 0);  // 1 day

    else

    {

        if (minutes <= 1.0)

            return new TimeSpan(0, 0, 5);   // 5 seconds

        else if (minutes <= 10.0)

            return new TimeSpan(0, 1, 0);   // 1 minute

        else if (hours <= 1.0)

            return new TimeSpan(0, 10, 0);  // 10 minutes

        else

            return new TimeSpan(1, 0, 0);   // 1 hour

    }

}

 

 The astute readers may notice that my best resolution is 5 seconds which could mean your event could execute up to almost 5 seconds after the desired date/time.  For my purposes this is more than sufficient; but, you may want to add additional graduated TimeSpan values within the CalculateDueTime helper method.  This timer class has shown itself to be efficient and reliable in my current project.  Hopefully, you'll find it useful in one of yours.

Here is the complete code for the class:

public class CalendarTimer : IDisposable

{

    private Timer timer;

    private TimeSpan noPeriod = new TimeSpan(0, 0, 0, 0, -1);

    private bool disposed;

 

    public CalendarTimer()

    {

    }

 

    ~CalendarTimer()

    {

        Dispose(false);

    }

 

    public event EventHandler TimerElapsed;

 

    public DateTime ElapseDate { get; private set; }

 

    public bool Started { get; private set; }

 

    public void Start(DateTime elapseDate)

    {

        if (disposed)

            throw new ObjectDisposedException(this.GetType().Name);

 

        TimeSpan dueTime = CalculateDueTime(elapseDate);

        ElapseDate = elapseDate;

        Started = true;

 

        if (timer == null)

        {

            TimerCallback callback = new TimerCallback(OnCheckDateTime);

            timer = new Timer(callback, null, dueTime, noPeriod);

        }

        else

        {

            timer.Change(dueTime, noPeriod);

        }

    }

 

    public void Stop()

    {

        if (disposed)

            throw new ObjectDisposedException(this.GetType().Name);

 

        Started = false;

        timer.Change(-1, 0);    // stops the timer

    }

 

    #region IDisposable Members

    public void Dispose()

    {

        Dispose(true);

        GC.SuppressFinalize(this);

    }

    #endregion

 

    private void OnCheckDateTime(object state)

    {

        TimeSpan timeDelta = ElapseDate.Subtract(DateTime.Now);

 

        if (timeDelta.TotalSeconds <= 0.0)

        {

            // the elapse date/time has been reached

            Stop();

            OnTimerElapsed(this, EventArgs.Empty);

        }

        else

        {

            // refine the due time for the internal timer

            TimeSpan newDueTime = CalculateDueTime(timeDelta);

            timer.Change(newDueTime, noPeriod);

        }

    }

 

    private void OnTimerElapsed(object sender, EventArgs e)

    {

        if (TimerElapsed != null)

            TimerElapsed(sender, e);

    }

 

    private void Dispose(bool disposing)

    {

        if (!disposed)

        {

            if (disposing)

            {

                if (timer != null)

                {

                    timer.Dispose();

                    timer = null;

                }

            }

 

            disposed = true;

        }

    }

 

    private static TimeSpan CalculateDueTime(DateTime elapseDate)

    {

        TimeSpan timeDelta = elapseDate.Subtract(DateTime.Now);

        return CalculateDueTime(timeDelta);

    }

 

    private static TimeSpan CalculateDueTime(TimeSpan nextRun)

    {

        double minutes = nextRun.TotalMinutes;

        double hours = nextRun.TotalHours;

 

        if (hours > 24)

            return new TimeSpan(24, 0, 0);  // 1 day

        else

        {

            if (minutes <= 1.0)

                return new TimeSpan(0, 0, 5);   // 5 seconds

            else if (minutes <= 10.0)

                return new TimeSpan(0, 1, 0);   // 1 minute

            else if (hours <= 1.0)

                return new TimeSpan(0, 10, 0);  // 10 minutes

            else

                return new TimeSpan(1, 0, 0);   // 1 hour

        }

    }

}

Posted on Thursday, January 28, 2010 10:55 AM .NET | Back to top


Comments on this post: Calendar-based Timer

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © Anthony Trudeau | Powered by: GeeksWithBlogs.net