Introduction
Working on a project, I found a neat way to make one of those infamous ASP.NET server-side calendar controls that look and work like a JavaScript control! This is an easy plug-in for any project! I included some functions like being able to have the calendar display over other controls, the ability to move through different months and keep the current calendar displayed; in essence, I made the ASP.NET server-side control work just like one of those slick JavaScript calendar objects out there on the net. It also keeps the control over select drop downs and other like controls (with one caveat, when you change months, it does not do this, much to my chagrin!).
Setup
To get this to work in your ASP.NET 2.0 (or you could even use it in 1.1) web application project, you first need to import Calendar.ascx (and of course, Calendar.ascx.cs) into your project in the usual place where you keep user controls. I put mine in a folder I named UserControls.
Next, to get the images to be seen no matter what level of the application you are in, I usually include the project name as a web.config application settings variable. This is useful when your application is not the root web application. If it is the root, then just strip this out of the calendar user control:
<configuration>
<appSettings>
<add key="ApplicationName" value="ASP2.0_Calendar"/>
</appSettings>
<connectionStrings/>
...the Calendar.ascx code:
<img border="0"
src="/<%= ConfigurationManager.AppSettings["ApplicationName"] %>/images/x.gif"
align="top" >
Now, you can use my style sheet and calendar theme, or make up your own for the calendar. To use mine, simply put the Calendar.skin file into your application's App_Themes/CalendarTheme folder.
Below is how the Solution Explorer paths look in the actual project.
You need to add the theme references to your page header as well:
<%@ Page Language="C#" ..... Theme="CalendarTheme"
Title="ASP2.0 Pop-Up Calendar Example" StylesheetTheme="CalendarTheme" %>
Now you are ready to add a calendar control to your page.
Using the Code
The first thing with adding a user control to any page in ASP.NET is to register the control with the page. This basically allows the type of control to be seen and allowed by the page. Visual Studio .NET will usually do this for you when you are in Designer mode and drop a control from your Solution Explorer directly on the page:
The control object can also be added manually by using the @Register
tag, as seen here:
<%@ Register Src="UserControls/Calendar.ascx" TagName="Calendar" TagPrefix="uc1" %>
Next, you want to add the actual control:
<uc1:Calendar ID="EndDateCalendar" runat="server" />
I made the row tag <td>
have an onclick
event so that if you clicked the text box or the little calendar icon, the calendar would popup. Then you can close it with an X box image on the calendar pop-up!
<td valign="top" align=left nowrap
onclick="javascript:setDisplay('<%=EndDateCalendar.ClientName %>',
false);setDisplay('<%=StartDateCalendar.ClientName %>',true);">
....Text Box and Image
</td>
Notice that each calendar object has a method to get its client name. I did this so we could merge JavaScript from the page with JavaScript from the control and make a call to the appropriate calendar's JavaScript display method.
Now, let's take a look at some important points of how the calendar works.
First, let's look at the additions you need to make to the page itself. I say page, but this could be any control, even another user control. I have added a method to allow the text box object to be registered with the calendar control. This allows some automatic exchange of data between your text box or hidden field and the calendar. Right now, only databinding the text box and changing the calendar object's date value will have any cross binding effect without adding an event on the page. I tried adding some code to capture the TextBox.OnTextChanged
events, but this didn't work. I suspect it was because the event is on the calendar control and not on the page. It is strange though, since the TextBox.OnDataBinding
event on the calendar does get called. Anyone out there know why this is?
protected void Page_Init(object sender, EventArgs e)
{
if (Page.IsPostBack) return;
this.StartDateCalendar.RegisterControl(ref StartDate);
this.EndDateCalendar.RegisterControl(ref EndDate);
this.Page.LoadComplete += new EventHandler(Page_LoadComplete);
}
We need to add some code to add an event handler for the Page.LoadComplete
event for the page or control the calendar is going to be displayed on. I do this because I want the calendar control to have its data loaded after everything else is done on the page. Whether or not you can make it work in a normal page load, I leave up to you.
I make the text box for each calendar control have as its text the calendar control's selected value. I do this so that when the page is reloaded after the calendar selection is changed, we get those values displayed in the text box. So when you select a value from the calendar control and the page refreshes, the value you chose is the value in the text box. After that, I check for a post back on the page, and if not (this is the initial load of the page), then I set both the text box and calendar control values. This is where you would fill a page with values from the data source.
An alternate way is to databind the text box control, which will call the TextBox_DataBinding
event handler on the calendar control. This frees up having to set the values on the calendar object specifically.
protected void Page_LoadComplete(object sender, EventArgs e)
{
StartDate.Text = StartDateCalendar.SelectedDate;
EndDate.Text = EndDateCalendar.SelectedDate;
if (Page.IsPostBack) return;
this.StartDate.Text = DateTime.Now.ToShortDateString();
this.StartDateCalendar.SelectedDate = this.StartDate.Text;
this.EndDate.Text = DateTime.Now.AddDays(12).ToShortDateString();
this.EndDateCalendar.SelectedDate = this.EndDate.Text;
}
this.StartDate.Text = DateTime.Now.ToShortDateString();
this.StartDate.DataBind();
this.EndDate.Text = DateTime.Now.AddDays(12).ToShortDateString();
this.EndDate.DataBind();
Next, we need to set up two events for when the text box control's text get changed by adding event handlers to the TextChanged
event. This will make sure that the calendar controls always have the same values as their text boxes. As I stated above, I could not get the TextBox_TextChanged
event for the registered text box control to work. I suspect it has something to do with its location on the calendar, but I don't know right now. Anyway, I do it here on the page where the text box control is, and it works fine...
<--the StartDate textbox on Default.aspx-->
<asp:TextBox ID="StartDate" runat="server"
MaxLength="10" OnTextChanged="StartDate_TextChanged"
ValidationGroup="EditGroup" Width="100px" ></asp:TextBox>
.......the EndDate textbox on Default.aspx
<asp:TextBox ID="EndDate" runat="server"
MaxLength="10" OnTextChanged="EndDate_TextChanged"
ValidationGroup="EditGroup" Width="100px" ></asp:TextBox>
The Default.cs file:
#region Calendar
protected void StartDate_TextChanged(object sender, EventArgs e)
{
StartDateCalendar.SelectedDate = StartDate.Text;
}
protected void EndDate_TextChanged(object sender, EventArgs e)
{
EndDateCalendar.SelectedDate = EndDate.Text;
}
#endregion
Now we can look at the user control.
The first thing we notice is the Page.Load
event handler, which checks for an attribute called IsMonthChanging
to be true to set up a hidden field. This sets the calendar object's value and state for the control. It also changes the registered control's value if the IsMonthChanging
value is false
:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsMonthChanging)
{
this.SelectedDate = DateCalendar.SelectedDate.ToShortDateString();
if (Page.FindControl(RegisteredControlName.Value) is TextBox)
{
((TextBox)FindRegisteredControl()).Text = this.SelectedDate;
}
else if (FindRegisteredControl() is HiddenField)
{
((HiddenField)FindRegisteredControl()).Value = this.SelectedDate;
}
}
}
We see below this, two methods to register either a text box or a hidden field with the calendar. This is here so we can data bind and have the calendar and the text box control automatically exchange their values:
public void RegisterControl(ref TextBox controlToRegister)
{
controlToRegister.DataBinding += new EventHandler(TextBox_DataBinding);
controlToRegister.AutoPostBack = true;
RegisteredControlName.Value = controlToRegister.ID;
}
public void RegisterControl(ref HiddenField controlToRegister)
{
controlToRegister.DataBinding += new EventHandler(HiddenField_DataBinding);
RegisteredControlName.Value = controlToRegister.ID;
}
The next thing we see is the IsMonthChanging
attribute which tells us that we are not changing the calendar value, but instead are changing the displayed month. This will not affect the actual value of the control:
protected bool IsMonthChanging
{
get { return Convert.ToBoolean(MonthChanging.Value); }
}
The ClientName
attribute gives us a reference for the client-side JavaScript outside the control to the control's client rendered name. This is a special name that we use for the entire control, based on the control's ClientId
attribute:
public string ClientName
{
get { return this.ClientID + "_Selectable"; }
}
The SelectedDate
attribute allows us to get or set the value for the actual date of the control. It uses a hidden literal field to maintain the state of the control and check for errors. If an input error occurs, it sets the control back to the last valid date value:
public string SelectedDate
{
get
{
if (DateValue.Value != "1/1/0001")
return DateValue.Value;
else
return null;
}
set
{
try
{
DateCalendar.SelectedDate = Convert.ToDateTime(value);
DateCalendar.VisibleDate = Convert.ToDateTime(value);
DateValue.Value = value;
}
catch (Exception)
{
DateCalendar.SelectedDate = Convert.ToDateTime(DateValue.Value);
DateCalendar.VisibleDate = Convert.ToDateTime(DateValue.Value);
}
}
}
Next are two event handlers: DateCalendar_SelectionChanged
, which serves to allow us to change the state and value of the control, and DateCalendar_VisibleMonthChanged
, which allows us to determine that we are only changing the server control's current displayed month, not the actual value:
protected void DateCalendar_SelectionChanged(object sender, EventArgs e)
{
DateValue.Value = DateCalendar.SelectedDate.ToShortDateString();
if (Page.FindControl(RegisteredControlName.Value) is TextBox)
{
((TextBox)FindRegisteredControl()).Text = this.SelectedDate;
}
else if (FindRegisteredControl() is HiddenField)
{
((HiddenField)FindRegisteredControl()).Value = this.SelectedDate;
}
if (Page.IsPostBack)
{
MonthChanging.Value = "False";
}
}
protected void DateCalendar_VisibleMonthChanged(object sender, MonthChangedEventArgs e)
{
MonthChanging.Value = "True";
}
Lastly are two event handlers: TextBox_DataBinding
, which serves to allow us to change the value of the control when the registered text box is bound with data, and HiddenField_DataBinding
, which is an alternate method for hidden fields and allows us to change the value of the control when the registered hidden field is bound with data:
protected void TextBox_DataBinding(object sender, EventArgs e)
{
this.SelectedDate = ((TextBox)FindRegisteredControl()).Text;
}
protected void HiddenField_DataBinding(object sender, EventArgs e)
{
this.SelectedDate = ((HiddenField)FindRegisteredControl()).Value;
}
In the ASCX file, we have some things that we need to look at too. The way we display or hide the control is determined by the boolean settings of its hidden field IsMonthChanging
. We set the client ID as the control's ClientName
attribute, and then in the style of the <div>
tag, we determine if the control is displayed or not by checking the IsMonthChanging
attribute. Also notice that the position
attribute of the div
style
is set to absolute
. This is done so that the calendar will float over the rest of the page. We also have an IFrame
which allows us to keep drop down controls from being displayed on top of the calendar:
<iframe id="<%= this.ClientName %>_overShelf"
scrolling="no" frameborder="0"
style="position:absolute; top:0px; left:0px; display:none;"></iframe>
<div id="<%= this.ClientName %>" style="z-index:99999; position:absolute; display:none" >
Our display function has the individual control's name, so we can call from outside the control the exact client JavaScript method name on the control. We set up in the client method how the IFrame
and the div
are displayed on the page:
function <%= this.ClientName %>
Running the Code
Now, let's run the code and examine how the control works at runtime. We see upon entry of the screen that our text box values are present inside their respective text boxes:
Next, if we click either the calendar icon or the text box, we see the calendar pop-up displayed just below the text box. Since the <div>
tag's style
property in the control has its position
set to absolute
, the control appears to float over the other controls. Also notice that the date in the text box is selected in red in the calendar:
Now if we wanted to change the calendar to another month, we can click the < or the > links on the calendar. This causes a post back just like on any other ASP.NET server-side calendar control, but when the page comes back up, the calendar won't disappear! It stays up until you click a value or the X icon at the top of the calendar! Pretty cool!
Now if you change the calendar's value, the calendar disappears and the text box gets that new value. If you were to again click on the text box or the calendar icon, you would see the calendar pop-up in the correct month, with the correct data selected!
Points of Interest
I tested this in IE only. I think cross browser, the client script to display the control will not work well. Probably a good refactor of this code would include some client-side code that works cross browser. If you send me this code improved in an acceptable manner, I will modify the article and give you your due credit for being the awesome CodeProject developer you are!
Also, there is room for improvement in how the text box and calendar objects co-exist (as stated above). Any good changes you wish to share will also be added, and the submitter will receive his or her just due on the article!
Thanks for reading!
Updates
- 01/25/06 - I have added some support for finding the controls in Master-Child page relationships.