A Sample of Date Selection with DateTimePicker
Introduction
DateTimePicker
Web control is similar to the windows DateTimePicker
control. The control exposes two properties: the SelectedDate
and the FormatType
. SelectedDate
represents the date user selected and FormatType
represents the format in which the date string should be displayed. The demo provided shows the usage of the control.
Background
Before getting on with the DateTimePicker
, I thought I would share with you some matters I found important while developing ASP.NET composite server controls:
Need for CreateChildControls:
This composite controls derives from System.Web.UI.WebControls.WebControl
as do most other composite controls. When developing composite controls, it is seldom that you don't need to override the function CreateChildControls
:
cal.VisibleMonthChanged +=new MonthChangedEventHandler(month_changed);
cal.SelectionChanged +=new System.EventHandler(date_changed);
Controls.Add(dArea);
Controls.Add(cal);
base.CreateChildControls();
In general, individual controls used by a composite control should be added to its Controls
collection. This has the effect of placing these constituent controls under the custom control in the control tree that ASP.NET framework generates for the page. A control tree is nothing but the equivalent of a DOM structure which the ASP.NET creates for a page. The image below illustrates a control tree for the page that housed the DateTimePicker
control.
Control Tree Structure
You can generate control tree information by enabling tracing on a page by adding trace="true"
to the Page
directive. The above figure represents the hierarchy of various elements present on the page. Adding the TextBox
and the Calendar
controls to the Controls
collection makes them the children of the composite control in the control tree hierarchy. The addition of constituent controls to the control tree makes sure that the events of the individual controls are suitably handled. We don't have to take extra measures for their event handling. All we need to do is add event handlers for the events we need to capture. CreateChildControls
is also the best place to add event handlers for the events you may want to receive from child controls, as shown in the code. But there is something more to all these. The question of when CreateChildControls
gets executed is important in successful event handling. This is where the INamingContainer
comes in.
Need for INamingContainer:
Almost all composite controls implement this interface. Implementing is simple as shown:
public class DateTimePicker : System.Web.UI.WebControls.WebControl.INamingContainer
INamingContainer
is a marker (empty) interface that does not have any methods. When this interface is implemented by a control, the ASP.NET page framework creates a new naming scope under that control. This ensures that child controls have unique IDs in the hierarchical tree of controls. This is evident in the control tree fig shown. The name for the control instance is dtpicker
and the name of the TextBox
is dtpicker_ctl0
and the name of the Calendar
control is dtpicker_ctl0
. Since there can be no other control on the page with name dtpicker
, we can be sure that the name of the TextBox
and Calendar
are unique. This feature is important when there are multiple instances of a control on a page. One thing to note is that this hierarchical naming scheme is important in event routing. If we give our own IDs to the child controls, then we won't be able to handle their events.
e.g. cal.ID="mycalendar";
INamingContainer
also controls as to when the CreateChildControls
method for a composite control is called. Consider the ASP.NET page execution cycle of a composite control without the INamingContainer
interface implementation:
LoadPostData->OnLoad->RaisePostDataChanged->
Handle events->OnPrerender->CreateChildControls->
SaveViewState->Render->Dispose
Now, compare the above with the following when the INamingContainer
is implemented:
OnInit-> LoadViewState->LoadPostData->RaisePostDataChanged->
CreateChildControls->OnLoad->Handle events->OnPrerender->
SaveViewState->Render->Dispose
The ASP.NET page execution cycle represents the various steps between the request arriving for an �aspx� page on the IIS and the rendering of the page to the browser. This cycle happens every time a page is requested, i.e., this happens the first time the page is loaded and for all subsequent postbacks. From the above two sequences, we see that INamingContainer
is important if we need to handle events of constituent controls. In the first sequence during the event handling phase, the child controls were not part of control tree, so the event was just ignored. (Only controls present in the control tree during event handling are candidates for event handling.) Thus we see that the naming scheme and control tree structure is central to event handling and implementing INamingContainer
guarantees that all is well.
Need for ViewState
As I discussed earlier, the ASP.NET page execution cycle is executed every time a request for a page arrives at the server. Since the control instance is created from scratch every time, the values of properties when the page was rendered the last time should be saved somewhere. This is precisely what is being achieved by the ViewState
property. The ViewState
is a dictionary object which stores name value pairs. When a page is rendered, the Viewstate
of each of the controls on the page is collectively rendered in the form of <input type=hidden>
. You can see this input control in the 'View Source' of the browser. When a page is postback, the data in the input field is used to recreate the ViewState
. This is evident in the page execution cycle shown above. During the OnInit
phase, the control instances are created (construction), and then during the LoadViewState
phase, each control is given its previous state which was saved in the page in the form of hidden control. Thus ViewState
helps us to create an effect of persistent storage. All we need to do is implement properties as returning value from ViewState
and writing into ViewState
as here:
public string SelectedDate
{
get
{
return (string)ViewState["selecteddate"];
}
set
{
ViewState["selecteddate"]=value;
}
}
public string FormatType
{
get
{
return (string)ViewState["format"];
}
set
{
string val=(string)value;
ViewState["format"]=val.ToUpper();
}
The SelectedDate
is to be entered in the format �mm/dd/yy�. The FormatType
can be �long� or �short�. If SelectedDate
is not specified or is given in some wrong format, then today's date is taken as the default.
Working of the DateTimePicker
The DateTimePicker
control is a composite control which includes two server side components: the TextBox
and the Calendar
control. It also includes a client side button control. The textbox and button are housed in one table and the calendar in another. The need for another table arises from the fact that we need to change its visibility on button click and on focus changes. In order to achieve this via JavaScript, we need to refer it by an ID. We can�t assign an ID to Calendar
control because then event handling is not possible. So we put it within a table and change the visibility of the outer table instead. All the controls are rendered at once, but the visibility of the Calendar
(and in fact, the outer table) varies according to the following:
- When the page containing
DateTimePicker
is loaded the first time, Calendar
is invisible.
- When the page is loaded after a postback which was initiated by a date change event, the
C
alendar
is invisible.
- When the page is loaded after a postback which was initiated by a month change event, the
Calendar
is visible.
When the calendar is visible at page load, we need to set its z-Index
property to some high value for achieving a 3D effect (the calendar appears over the controls which it overlaps). But for this, we need a control which has an OnLoad
event. Since we haven�t used one such control till now, I simply generate a dummy IFrame
and do the housekeeping works in the OnLoad
event of IFRAME
. There�s also an input hidden control that is generated which specifies whether the Calendar
is visible or otherwise. This control also helps in back button handling. We won�t look into the details of the JavaScript function. You can find these functions in the Render
method. I have commented it to my best and hopefully it will be enough to get a feel of how the controls work. You may have now realized as to why I overloaded the Render
method within the control, since I had to output some extra controls and their supporting JavaScript functions. If you have only your child server controls to render in a composite control, there is no need for the render method. One thing to remember in this case is that there can be several instances of one control on a page. So HTML component names created in the Render
method should be unique:
This is achieved by generating the names by appending this.UniqueID
to function and component names. This is how it was done here:
string uniqueID=this.UniqueID;
string spanID="main"+uniqueID;
string btnID=uniqueID+"btn";
string tdID=uniqueID+"td;
string hidID=uniqueID+"_a1;
this.UniqueID
represents the name your control as given in the page it is used. Since no two controls can have the same name component, names are bound to be unique.
Implementing Design Time Support
Implementing design-time support involves writing a new class which overrides the class System.ComponentModel.Design.ControlDesigner
.
The members that need to be overridden include GetDesignTimeHtml
and AllowResize
. Some portion of the class implementation is shown below:
public class MyDesigner:ControlDesigner
{
string width;
public override bool AllowResize
{
get
{
return true;
}
}
public override string GetDesignTimeHtml()
{
string designTimeHtml;
DateTimePicker controlDesigned=(DateTimePicker)this.Component;
string dateDesignTime=controlDesigned.SelectedDate;
string format=controlDesigned.FormatType;
...
}
This is the function which is called by the designer to get the �HTML� which represents your control in the design view. This function is called when a control is placed on a page in the design view and every time the control is moved or resized. The designer generates the visual form for the HTML returned from the function. The component
property returns a reference to the control instance that is being designed. There may be some properties whose values we may need to render at design time. These values can be obtained by the instance pointed by the component
property.
AllowResize
method simply returns true
if your control supports resizing. In order to inform the designer that MyDesigner
is the designer class for this control, simply add the following attribute to the DateTimePicker
class:
[Designer("DTPicker.MyDesigner")]
public class DateTimePicker :
Implementing IntelliSense Support
IntelliSense support is of use when one is creating an aspx page without the design view, i.e., you are typing out the entire content of the .aspx page.
The figure below illustrates the intellisense support for this control:
Intellisense support is implemented by writing a schema file which defines the attributes your control can have and the various elements that can come within its beginning and closing tag. (Here there are no elements). I won�t go into the details of writing a schema file. One can find enough samples in the web. The only thing to remember is that the targetNamespace
exposed in the schema should be the same as the namespace of the control.
The namespace in the DLL and targetNamespace
of schema file are both DTPicker
. The schema file for this control is provided in the download section.
The next step is to copy the schema file into the folder:
C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Packages\schemas\xml.
This is where the schema for all your server controls reside.
About the Samples
The DateTimePickerControl
code in the download is the project folder of the control. After downloading, compile the project to generate the DateTimePicker
DLL. If you are using the control from the design view, simply add the DLL to the ToolBox. The properties SelectedDate
and FormatType
can be set from the Properties window.
If you don�t use the Design View, then add a reference to the DateTimePicker
DLL in the ASP.NET project. Now, add the following Register
directive.
<%@ Register TagPrefix="dtp" Namespace="DTPicker" Assembly="DTPicker" %>
When you add the first instance of the control in Design View, this directive is automatically added to the page. But the TagPrefix
in this directive generated by the designer will be some value like �cc1�,�cc2�, etc.
The name of the control is DateTimePicker
, so the following tag represents our control.
<dtp:DateTimePicker>
In the Design View, you may have the name <cc1:DateTimePicker>
or something similar depending on the TagPrefix
.
Implementing IntelliSense support involves adding the following attribute to the <HTML>
tag of the page housing the control.
<HTML xmlns:dtp=�DTPicker�>
The above line assumes that the TagPrefix
for the control is dtp
. If TagPrefix
is cc1
then the HTML
tag changes to:
<HTML xmlns:cc1=�DTPicker�>
DTPicker
is the targetNamespace
of the schema file.
There is a sample aspx page and code-behind page provided in the download section as an example of usage of the control.