Click here to Skip to main content
15,892,005 members
Articles / Web Development / ASP.NET
Article

DHTML Clock Control

Rate me:
Please Sign up or sign in to vote.
4.52/5 (13 votes)
24 May 20044 min read 60.9K   1.8K   26  
Digital DHTML clock that displays a .NET DateTimeFormatInfo Format Pattern.

Sample Image - DHTMLDigitalClock.gif

Introduction

This little widget started life as a simple idea and escalated into total overkill for the niche it was originally intended. Never mind - I learnt a thing or two along the way... I wanted a DHTML clock that would update dynamically, but one that would take a .NET DateTimeFormatInfo Format Pattern as an argument, so allowing for a fair bit of scope in how it displayed. I also needed it to be regionally independent, so relying on the developer to decide globalization settings.

Server Side

The actual web control is simple enough. There are 2 public properties, DateTimeFormat which is the string to be split into the datetime/literal pattern, and Interval which is an integer representing the amount of time in milliseconds to wait before updating the clock again. Internally, the private string array DateTimeFormatArray takes the DateTimeFormat string and splits it using the method FormatPatternSplitter.

FormatPattern Splitter

FormatPatternSplitter starts off by checking if the input string is an escaped format character, and just returns it if it is:

C#
if(s.Length==2)
{
  if(s.Substring(0,1)=="%")
  {
    return s.Substring(1,1).Split( );
  }
}

It then checks whether the input string is an unescaped format character and if so converts it to the corresponding format pattern:

C#
if(s.Length==1)
{
  DateTimeFormatInfo dtfi = new DateTimeFormatInfo();
  s = dtfi.GetAllDateTimePatterns(Convert.ToChar(s))[0];

  if(s.Length==0)
  {
    return s.Split( );
  }
}

Now, we match the resulting string of format patterns and literals against this regular expression. (The black art of regular expressions is sometimes a bit beyond me. If anyone can think of a more efficient way of executing this, I'd be very interested to know...)

C#
System.Text.RegularExpressions.Regex rgx = 
  new Regex(@"(?<dtf>(d{1,4})|(M{1,4})|(y{1,4})|(gg)" + 
             "|(h{1,2})|(H{1,2})|(m{1,2})|(s{1,2})|(f{1,7})|" + 
             "(t{1,2})|(z{1,3}))|(?<spc>(\s+))|" + 
             "(?<spt>([^\1]))");
System.Text.RegularExpressions.MatchCollection rgxMatches = rgx.Matches(s);

We then loop through the matches checking if any characters have been escaped and adding the results to an ArrayList, then finally returning the resulting array.

C#
foreach(Match rgxMatch in rgxMatches)
{
  match = rgxMatch.Value;

  if(match==@"\")
  {
    if(backslashFlag)
    {
      formatPattern.Add(@"\\");
    }
    backslashFlag = true;
  }
  else
  {
    if(backslashFlag)
    {
      formatPattern.Add(@"\" + match);
    }
    else
    {
      formatPattern.Add(match);
    }
    backslashFlag = false;
  }
  backslashFlag = (match==@"\");
}

return (string[])formatPattern.ToArray(System.Type.GetType("System.String"));

Back at the control, the resulting string array is given a few tweaks to make sure that the client side JavaScript doesn't choke (i.e.: escaping single quotes and backslashes).

C#
for(int i=0;i<dateTimeFormatArray.Length;i++)
{
  if((dateTimeFormatArray[i]!=null)&&(dateTimeFormatArray[i].IndexOf("'")==-1))
  {
    dateTimeFormatArray[i] = dateTimeFormatArray[i].Replace(@"\", @"\\");
  }
}

Overriding OnPreRender

I have used Paul Riley's JavaScript builder class to setup the client side code, which looks like this:

C#
#if DEBUG
HYL.Utilities.Javascript.JavaScriptBuilder jsb = 
  new HYL.Utilities.Javascript.JavaScriptBuilder(true);
#else
HYL.Utilities.Javascript.JavaScriptBuilder jsb = 
  new HYL.Utilities.Javascript.JavaScriptBuilder();
#endif

DateTimeFormatInfo dtfi = new DateTimeFormatInfo();

jsb.AddLine("var aDayNames =", 
  HYL.Utilities.Javascript.JavascriptUtilities.FormatAsArray(dtfi.DayNames), ";");
jsb.AddLine("var aMonthNames = ", 
  HYL.Utilities.Javascript.JavascriptUtilities.FormatAsArray(dtfi.MonthNames), ";");
jsb.AddLine("var sAMDesignator = '", dtfi.AMDesignator, "';");
jsb.AddLine("var sPMDesignator = '", dtfi.PMDesignator, "';");
jsb.AddLine("var sEraName = '", 
  dtfi.GetEraName(dtfi.Calendar.GetEra(System.DateTime.Today)), "';");

System.Text.StringBuilder sb = new System.Text.StringBuilder();

sb.Append(jsb.ToString());

sb.Append("<SCRIPT language='\"javascript\"' src='\"clock.js\"'></SCRIPT>");

this.Page.RegisterClientScriptBlock(pageJSKey, sb.ToString());

I'd definitely recommend taking a look at Paul's article, it simplifies writing client side code in your controls which as you're probably aware can rapidly turn into a messy business. Note that it's here that we declare the globalization specific variables, before the clock script file is rendered. Because this lump of code is registered using the Page.RegisterClientScriptBlock method, it ensures that it will only appear once on the page.

Overriding Render

We then render the control. First, the span tag that will hold the clock, which we fill with the formatted datetime string:

C#
output.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
output.RenderBeginTag(HtmlTextWriterTag.Span);
output.Write("{0:" + this.DateTimeFormat + "}", DateTime.Now);
output.RenderEndTag();

Then, we create a new client side Clock instance, passing the control's ClientID property, so the control can be identified by the client side code; the format pattern, as a JavaScript array; and the optional update interval argument, which if omitted will default to 1000 milliseconds, or for those of us without a Mathematics degree, 1 second.

C#
output.Write("<SCRIPT language='\"javascript\"'><!--\n");
output.Write("new Clock('");
output.Write(this.ClientID);
output.Write("', ");
output.Write(HYL.Utilities.Javascript.JavascriptUtilities.FormatAsArray(
                                              this.DateTimeFormatArray));
output.Write(", ");
output.Write(this.Interval.ToString());
output.Write(");");
output.Write("\n//--></SCRIPT>");

All that needs to be done now (at least on the server side anyway) is to include a reference in your web form:

ASP.NET
<hyl:clock id="clock1" runat="server" 
DateTimeFormat="\hello, \t\he \da\te i\s dd.MM.yyyy"></hyl:clock>

Client Side

The client side object has 2 methods.

Load uses the window.setInterval method to trigger the Update method every n milliseconds (where n is the update interval property).

JavaScript
this.Load = function()
{
  setInterval('aClockCollection['+this.Index+'].Update()', this.Interval);
}

The Update method then rewrites the inner HTML of the control with the results of the GetDateTime function.

JavaScript
this.Update = function()
{
  var el;
  if(document.getElementById)
  {
    el = document.getElementById(this.ReturnElement);
    el.innerHTML = ''; // Mac IE5.1+ bugfix by Richard Meijer
    el.innerHTML = GetDateTime(this.FormatPattern);
  }
  else if(document.all)
  {
    el = document.all[this.ReturnElement];
    el.innerHTML = GetDateTime(this.FormatPattern);
  }
}

Notice the relative simplicity and elegance when NN4 is totally ignored... anyway, the GetDateTime function creates an array populated with the datetime format characters at the current time:

JavaScript
var d                     = now.getDate();
aFormatPattern['d']       = d;
aFormatPattern['dd']      = (d<=9)?'0'+d:d;
var iDay                  = now.getDay();
aFormatPattern['ddd']     = aDayNames[iDay].substr(0,3);
aFormatPattern['dddd']    = aDayNames[iDay];
var M                     = now.getMonth();
aFormatPattern['MMM']     = aMonthNames[M].substr(0,3);
aFormatPattern['MMMM']    = aMonthNames[M];
M++;
aFormatPattern['M']       = M;
aFormatPattern['MM']      = (M<=9)?'0'+M:M;
var yyyy                  = now.getFullYear();
aFormatPattern['yyyy']    = yyyy;
aFormatPattern['yy']      = (''+yyyy).substring(2);
aFormatPattern['y']       = Math.abs(aFormatPattern['yy']);
aFormatPattern['g']       = sEraName;
var H                     = now.getHours();
aFormatPattern['H']       = H;
aFormatPattern['HH']      = (H<=9)?'0'+H:H;
var h                     = (H>12)?H-12:H;
aFormatPattern['h']       = h;
aFormatPattern['hh']      = (h<=9)?'0'+h:h;
var m                     = now.getMinutes();
aFormatPattern['m']       = m;
aFormatPattern['mm']      = (m<=9)?'0'+m:m;
var s                     = now.getSeconds();
aFormatPattern['s']       = s;
aFormatPattern['ss']      = (s<=9)?'0'+s:s;
var f                     = now.getMilliseconds()+'000000';
aFormatPattern['f']       = f.substr(0,1);
aFormatPattern['ff']      = f.substr(0,2);
aFormatPattern['fff']     = f.substr(0,3);
aFormatPattern['ffff']    = f.substr(0,4);
aFormatPattern['fffff']   = f.substr(0,5);
aFormatPattern['ffffff']  = f.substr(0,6);
aFormatPattern['fffffff'] = f.substr(0,7);
var tt                    = ((H>=0)&&(H<12))?sAMDesignator:sPMDesignator;
aFormatPattern['tt']      = tt;
aFormatPattern['t']       = tt.substr(0,1);
var z                     = now.getISOTimezoneOffset();
aFormatPattern['z']       = z.substr(0,1)+(''+parseInt(z.substr(1,2)));
aFormatPattern['zz']      = z.substr(0,1)+(''+z.substr(1,2));
aFormatPattern['zzz']     = z;

Note that the index of each element is the format character, this will make things a lot easier later on. Also, note the new getISOTimezoneOffset method which extends the native Date object prototype. The JavaScript method getTimezoneOffset just confuses me and appears to be a bit buggy anyway, so this new method returns a timezone offset that matches up with .NET's:

JavaScript
function getISOTimezoneOffset()
{
  var dif  = this.getHours()-this.getUTCHours();
  var hDif = Math.abs(dif);
  var m    = this.getMinutes();
  var mUTC = this.getUTCMinutes();

  if(m!=mUTC&&mUTC<30&&dif<0){ hDif--; }
  if(m!=mUTC&&mUTC>30&&dif>0){ hDif--; }

  return(((dif<0)?'-':'+')+((hDif<10)?'0'+hDif:''+hDif)+':'+((m!=mUTC)?'30':'00'));
}
Date.prototype.getISOTimezoneOffset = getISOTimezoneOffset;

The GetDateTime function takes the clock's format pattern as an argument, so all we need to do now is loop through that, using the elements of the format pattern as the index for the current time format pattern array, building up a return string as we go:

JavaScript
var sBackslash = String.fromCharCode(92);

for(i=0;i<_formatPattern.length;i++)
{
  if(_formatPattern[i].substr(0,1)==sBackslash)
  {
    sRetVal += _formatPattern[i].substring(1);
  }
  else
  {
    sPattern = aFormatPattern[_formatPattern[i]];
    if(typeof(sPattern)!='undefined')
    {
      sRetVal += sPattern;
    }
    else
    {
      sRetVal += _formatPattern[i];
    }
  }
}
return sRetVal;

Notice that we check if the input character has been escaped with a backslash and display it literally if it has.

All that's left to do is initialize the clock(s) on the page in the onload handler:

JavaScript
var aClockCollection = new Array();

function InitClocks()
{
  for(var clockCount=0;clockCount<aClockCollection.length;clockCount++)
  {
    aClockCollection[clockCount].Load();
  }
}

safeAddEventListener(window, 'load', InitClocks, false);

The array aClockCollection holds all the clocks on the page (you may have noticed the reference to it in the Clock object). InitClocks loops through this array and calls the Load method for each one. The function safeAddEventListener is simply a cross browser wrapper utility for triggering methods, and is in the source.

Summary

As I mentioned before, a complete overkill if all you want is a simple clock ticking away, but I have used the FormatPatternSplitter in a couple of other projects and I quite like the simplicity of the .NET format pattern model, so not a complete loss all in all.

Brown Nose

I hope bits (or all!) of this are helpful - this is the first article I've written for Code Project, and I'd have to credit a substantial part of my C# and .NET knowledge to the various articles I have read on it in the last year.

Cheers!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
bne
Web Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --