ASP 服务器端类似 JavaScript 的日历弹出






4.03/5 (12投票s)
一个臭名昭著的 ASP.NET 服务器端日历控件,它看起来和工作方式都像一个 JavaScript 控件!这是任何项目的便捷插件!我包含了一些功能,例如能够让日历显示在其他控件之上,以及能够浏览不同月份并保持
引言
在做一个项目时,我找到了一种巧妙的方法来创建一个臭名昭著的 ASP.NET 服务器端日历控件,它看起来和工作方式都像一个 JavaScript 控件!这是任何项目的便捷插件!我包含了一些功能,例如能够让日历显示在其他控件之上,以及能够浏览不同月份并保持当前日历的显示;本质上,我让 ASP.NET 服务器端控件的工作方式就像网络上那些时尚的 JavaScript 日历对象一样。它还能够控制下拉列表和其他类似控件(有一个例外,当你切换月份时,它做不到这一点,这让我非常沮丧!)。
安装
要在你的 ASP.NET 2.0(甚至可以在 1.1 中使用)Web 应用程序项目中实现此功能,你首先需要将 *Calendar.ascx*(当然还有 *Calendar.ascx.cs*)导入到你的项目中,通常是你存放用户控件的位置。我将它们放在了一个名为 *UserControls* 的文件夹中。
接下来,为了让图片无论在应用程序的哪个级别都能显示,我通常会将项目名称作为 *web.config* 应用程序设置变量包含在内。当你的应用程序不是根 Web 应用程序时,这非常有用。如果它是根应用程序,那么只需从日历用户控件中删除此部分。
<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" ></a>
现在,你可以使用我的样式表和日历主题,或者为日历创建自己的主题。要使用我的主题,只需将 *Calendar.skin* 文件放入你应用程序的 *App_Themes/CalendarTheme* 文件夹即可。
下面是实际项目中解决方案资源管理器路径的外观。
你还需要将主题引用添加到你的页面标头中。
<%@ Page Language="C#" ..... Theme="CalendarTheme"
Title="ASP2.0 Pop-Up Calendar Example" StylesheetTheme="CalendarTheme" %>
现在你就可以在页面上添加日历控件了。
Using the Code
在 ASP.NET 中将用户控件添加到任何页面时,首先要做的是向页面注册该控件。这基本上允许页面识别并允许使用该类型的控件。当你从解决方案资源管理器将控件直接拖放到页面上时,Visual Studio .NET 通常会自动为你完成此操作。
也可以通过使用 *@Register* 标签手动添加控件对象,如下所示。
<%@ Register Src="UserControls/Calendar.ascx" TagName="Calendar" TagPrefix="uc1" %>
接下来,你想添加实际的控件。
<uc1:Calendar ID="EndDateCalendar" runat="server" />
我将行标签 `
<td valign="top" align=left nowrap
onclick="javascript:setDisplay('<%=EndDateCalendar.ClientName %>',
false);setDisplay('<%=StartDateCalendar.ClientName %>',true);">
....Text Box and Image
</td>
请注意,每个日历对象都有一个获取其客户端名称的方法。我这样做是为了让我们能够将页面中的 JavaScript 与控件中的 JavaScript 合并,并调用相应日历的 JavaScript 显示方法。
现在,让我们来看看日历工作的一些重要方面。
首先,让我们看看你需要对页面本身进行的添加。我说页面,但这可以是任何控件,甚至是另一个用户控件。我添加了一个方法来允许将文本框对象注册到日历控件。这允许在你的文本框或隐藏字段与日历之间进行一些自动数据交换。目前,只有在文本框数据绑定和更改日历对象的日期值时,才会产生跨绑定效果,而无需在页面上添加事件。我尝试添加了一些代码来捕获 `TextBox.OnTextChanged` 事件,但没有成功。我怀疑是因为事件在日历控件上而不是在页面上。不过这很奇怪,因为日历上的 `TextBox.OnDataBinding` 事件确实会被调用。有人知道原因吗?
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);
}
我们需要向日历将要显示的页面或控件添加一些代码,以添加 `Page.LoadComplete` 事件的处理程序。我这样做是因为我希望日历控件的数据在页面上其他所有内容都完成后加载。至于是否能在常规页面加载中实现,我留给你自己尝试。
我让每个日历控件的文本框显示日历控件的选中值。我这样做是为了当页面在日历选择更改后重新加载时,我们能够将这些值显示在文本框中。所以当你从日历控件中选择一个值并且页面刷新时,你选择的值就是文本框中的值。在那之后,我检查页面是否是回发,如果不是(这是页面的初始加载),那么我就设置文本框和日历控件的值。你可以在这里从数据源填充页面。
另一种方法是对文本框控件进行数据绑定,这将调用日历控件上的 `TextBox_DataBinding` 事件处理程序。这样就不需要专门设置日历对象的值了。
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;
}
//.....Alternate way
this.StartDate.Text = DateTime.Now.ToShortDateString();
this.StartDate.DataBind();
this.EndDate.Text = DateTime.Now.AddDays(12).ToShortDateString();
this.EndDate.DataBind();
接下来,我们需要通过添加 `TextChanged` 事件的事件处理程序来为文本框控件的文本更改设置两个事件。这将确保日历控件始终具有与其文本框相同的值。如上所述,我无法让已注册文本框控件的 `TextBox_TextChanged` 事件正常工作。我怀疑这与它在日历上的位置有关,但目前我还不清楚。无论如何,我在这里文本框控件所在的页面上进行了设置,它工作得很好……
<--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>
Default.cs 文件
#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
现在我们可以看一下用户控件了。
我们首先看到的是 `Page.Load` 事件处理程序,它检查一个名为 `IsMonthChanging` 的属性是否为 true 以设置一个隐藏字段。这会设置控件的日历对象的值和状态。如果 `IsMonthChanging` 的值为 `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;
}
}
}
在此下方,我们看到了两个方法,用于注册文本框或隐藏字段到日历。这样做是为了我们可以进行数据绑定,并且日历和文本框控件可以自动交换它们的值。
public void RegisterControl(ref TextBox controlToRegister)
{
//controlToRegister.TextChanged += new EventHandler(TextBox_TextChanged);
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;
}
接下来我们看到 `IsMonthChanging` 属性,它告诉我们我们没有更改日历的值,而是更改了显示的月份。这不会影响控件的实际值。
protected bool IsMonthChanging
{
get { return Convert.ToBoolean(MonthChanging.Value); }
}
`ClientName` 属性为我们提供了控件外部客户端 JavaScript 对控件客户端渲染名称的引用。这是一个特殊的名称,我们根据控件的 `ClientId` 属性为整个控件使用它。
public string ClientName
{
get { return this.ClientID + "_Selectable"; }
}
`SelectedDate` 属性允许我们获取或设置控件实际日期的值。它使用一个隐藏的字面量字段来维护控件的状态并检查错误。如果发生输入错误,它会将控件恢复到上一个有效日期值。
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);
}
}
}
接下来是两个事件处理程序:`DateCalendar_SelectionChanged`,用于允许我们更改控件的状态和值,以及 `DateCalendar_VisibleMonthChanged`,用于允许我们确定我们只更改了服务器控件当前显示的月份,而不是实际值。
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";
}
最后是两个事件处理程序:`TextBox_DataBinding`,用于允许我们在注册的文本框与数据绑定时更改控件的值,以及 `HiddenField_DataBinding`,这是一个隐藏字段的替代方法,允许我们在注册的隐藏字段与数据绑定时更改控件的值。
/*protected void TextBox_TextChanged(object sender, EventArgs e)
{
this.SelectedDate = ((TextBox)FindRegisteredControl()).Text;
}*/
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;
}
在 ASCX 文件中,我们也有一些需要查看的内容。我们显示或隐藏控件的方式由其隐藏字段 `IsMonthChanging` 的布尔设置决定。我们将客户端 ID 设置为控件的 `ClientName` 属性,然后在 `
<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" >
我们的显示函数具有各个控件的名称,这样我们就可以从控件外部调用控件上确切的客户端 JavaScript 方法名称。我们在客户端方法中设置了 `IFrame` 和 `div` 在页面上的显示方式。
function <%= this.ClientName %>_SetDisplay(doDisplay)
{
if(doDisplay == true)
{
document.getElementById('<%= this.ClientName %>').style.display='inline';
document.getElementById('<%= this.ClientName %>_overShelf').style.zIndex =
document.getElementById('<%= this.ClientName %>').style.zIndex - 1;
document.getElementById('<%= this.ClientName %>_overShelf').style.width =
document.getElementById('<%= this.ClientName %>').offsetWidth;
document.getElementById('<%= this.ClientName %>_overShelf').style.height =
document.getElementById('<%= this.ClientName %>').offsetHeight;
document.getElementById('<%= this.ClientName %>_overShelf').style.top =
document.getElementById('<%= this.ClientName %>').offsetTop;
document.getElementById('<%= this.ClientName %>_overShelf').style.left =
document.getElementById('<%= this.ClientName %>').offsetLeft;
document.getElementById('<%= this.ClientName %>_overShelf').style.x =
document.getElementById('<%= this.ClientName %>').offsetTop;
document.getElementById('<%= this.ClientName %>_overShelf').style.left =
document.getElementById('<%= this.ClientName %>').offsetLeft;
document.getElementById('<%= this.ClientName %>_overShelf').style.display = 'block';
document.getElementById('<%= MonthChanging.ClientID %>').value="True";
}
else
{
document.getElementById('<%= this.ClientName %>').style.display='none';
document.getElementById('<%= this.ClientName %>_overShelf').style.zIndex =
document.getElementById('<%= this.ClientName %>').style.zIndex - 1;
document.getElementById('<%= this.ClientName %>_overShelf').style.width =
document.getElementById('<%= this.ClientName %>').offsetWidth;
document.getElementById('<%= this.ClientName %>_overShelf').style.height =
document.getElementById('<%= this.ClientName %>').offsetHeight;
document.getElementById('<%= this.ClientName %>_overShelf').style.top =
document.getElementById('<%= this.ClientName %>').offsetTop;
document.getElementById('<%= this.ClientName %>_overShelf').style.left =
document.getElementById('<%= this.ClientName %>').offsetLeft;
document.getElementById('<%= this.ClientName %>_overShelf').style.x =
document.getElementById('<%= this.ClientName %>').offsetTop;
document.getElementById('<%= this.ClientName %>_overShelf').style.left =
document.getElementById('<%= this.ClientName %>').offsetLeft;
document.getElementById('<%= this.ClientName %>_overShelf').style.display = 'none';
document.getElementById('<%= MonthChanging.ClientID %>').value="False";
}
运行代码
现在,让我们运行代码并检查控件在运行时如何工作。我们看到屏幕一进入,文本框中的值就显示在各自的文本框中。
接下来,如果我们点击日历图标或文本框,我们会看到日历弹出显示在文本框的正下方。由于控件中 `
现在,如果我们想将日历更改为另一个月份,我们可以点击日历上的 `<` 或 `>` 链接。这会像任何其他 ASP.NET 服务器端日历控件一样触发回发,但当页面再次出现时,日历不会消失!它会一直显示,直到你点击一个日期或日历顶部的 X 图标!很酷!
现在,如果你更改日历的值,日历会消失,文本框会获得这个新值。如果你再次点击文本框或日历图标,你将看到日历弹出显示在正确的月份,并且正确的数据已选中!
关注点
我只在 IE 中进行了测试。我认为跨浏览器的话,显示控件的客户端脚本可能无法很好地工作。也许对这段代码进行一次好的重构会包含一些能够跨浏览器工作的客户端代码。如果你以一种可接受的方式向我发送改进后的代码,我将修改文章,并赋予你作为一名出色的 CodeProject 开发者的应有荣誉!
此外,文本框和日历对象如何协同工作(如上所述)还有改进的空间。任何你想分享的好的更改也将被添加,并且提交者将获得他们在文章中的应有荣誉!
感谢阅读!
更新
- 01/25/06 - 我已添加了一些支持,用于在母子页面关系中查找控件。