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:
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:
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...)
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.
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).
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:
#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:
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.
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:
<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).
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.
this.Update = function()
{
var el;
if(document.getElementById)
{
el = document.getElementById(this.ReturnElement);
el.innerHTML = '';
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:
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:
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:
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:
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!
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.