65.9K
CodeProject 正在变化。 阅读更多。
Home

BetterCalendar WebControl

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (25投票s)

2004年4月19日

9分钟阅读

viewsIcon

260231

downloadIcon

4621

用于替换 System.Web.UI.WebControls.Calendar 控件。

Sample Image - BetterCalendar.gif

引言

BetterCalendar 是一个派生自 System.Web.UI.WebControls.Calendar 的自定义控件。它旨在纠正 Calendar 控件的一些问题并添加一些附加功能。

背景

Calendar 控件非常有用,但它有一些缺陷和缺点

  • 无法仅使用样式表设置日历的外观(即,将类名分配给各种日历样式元素的 CssClass 属性)。这是因为当样式属性采用默认值时,控件会为许多元素嵌入内联 style 属性。

    例如,日单元格的 HTML 可能如下所示

    <td class="calendarDay" align="Center" style="width:12%;">
    <a href="javascript:__doPostBack('Calendar1','1566')"
    style="color:Black">15</a></td>

    在这里,style 属性的颜色设置将覆盖 calendarDay 类中的任何颜色设置。

  • 虽然您可以根据某些条件为某一天定义不同的样式——通过 DayStyleWeekendDayStyleOtherMonthDayStyleTodayDayStyleSelectedDayStyle 属性——即使适用多个样式,也只分配一个样式类。

    换句话说,如果今天是周末并且在控件上被选中,则 SelectedDayStyle 样式优先

    <td class="selectedDay_style" ... >

    由于 HTML 元素可以在其 class 属性中分配多个类名,因此可以像这样包含所有适用的样式类

    <td class="day_style weekendDay_style todayDay_style selectedDay_style" ... >

    这将使您可以通过样式表完全控制其外观。

  • 使用 DayRender 事件,您可以控制哪些日期可选,哪些不可选。但是,当控件上周或月选择器处于活动状态时,给定周或月中的所有日期都包含在选择中,无论它们是否可单独选择。您可以使用 SelectionChanged 事件手动删除此类日期,但这会做两次相同的工作。

  • 最后,下一个月和上一个月的导航控件没有限制。您可以通过 VisibleMonthChanged 事件以编程方式阻止查看某个范围之外的月份,但下一个月和上一个月的链接始终呈现在控件上。

BetterCalendar 控件解决了这些问题。

使用代码

该控件打包在一个类库中,其命名空间为 BrainJar.Web.UI.WebControls,其中只包含一个类 BetterCalendar

在 Visual Studio Web 项目中,您只需添加对 BrainJar.Web.UI.WebControls.dll 文件的引用。然后,您可以在 HTML 视图中编辑 Web 窗体并添加适当的 @ Register 指令以及标签。下面显示了一个典型示例(请注意包含外部样式表)。

<%@ Page language="c#" Codebehind="WebForm2.aspx.cs" AutoEventWireup="false"
    Inherits="Demo.WebForm2" %>
<%@ Register TagPrefix="BrainJar" Namespace="BrainJar.Web.UI.WebControls"
    Assembly="BrainJar.Web.UI.WebControls"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > 
<HTML>
  <HEAD>
    <title>WebForm1</title>
    <meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">
    <meta name="CODE_LANGUAGE" Content="C#">
    <meta name=vs_defaultClientScript content="JavaScript">
    <meta name=vs_targetSchema
          content="http://schemas.microsoft.com/intellisense/ie5">
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
    <link href="Styles.css" type="text/css" rel="stylesheet">
  </HEAD>
  <body>
  
    <form id="WebForm1" method="post" runat="server">

    <h1>BetterCalendar Control  Demo</h1>
    <BrainJar:BetterCalendar id="BetterCalendar1" runat="server">
      <DayStyle CssClass="calendarDay"></DayStyle>
      <DayHeaderStyle CssClass="calendarDayHeader"></DayHeaderStyle>
      <NextPrevStyle CssClass="calendarNextPrev"></NextPrevStyle>
      <OtherMonthDayStyle CssClass="calendarOtherMonthDay">
      </OtherMonthDayStyle>
      <SelectedDayStyle CssClass="calendarSelectedDay"></SelectedDayStyle>
      <SelectorStyle CssClass="calendarSelector"></SelectorStyle>
      <TitleStyle CssClass="calendarTitle"></TitleStyle>
      <TodayDayStyle CssClass="calendarTodayDay"></TodayDayStyle>
      <WeekendDayStyle CssClass="calendarWeekendDay"></WeekendDayStyle>
    </BrainJar:BetterCalendar>

     </form>
  
  </body>
</HTML>

您也可以按照通常的方式将控件添加到工具箱(在设计模式下,右键单击工具箱,选择“自定义工具箱...”,选择“.NET Framework 组件”选项卡,按下“浏览”按钮并选择 BrainJar.Web.UI.WebControls.dll 文件)。

由于 BetterCalendar 派生自 Calendar 控件,因此它继承了该控件的所有属性、方法和事件。除此之外,它还定义了四个新属性

  • SelectAllInRange (Boolean)

    用于周和月选择器(请参阅 Calendar.SelectionMode 属性文档)。如果设置为 true,则选择给定周或月中的所有日期。当设置为 false 时,只包含可单独选择的日期。默认值为 false。请参阅演示项目,了解这如何影响控件的示例。

  • ShowLinkTitles (Boolean)

    将此属性设置为 true 会为控件上呈现的链接添加 title 属性,其中包含描述其功能的文本。例如,上一个月的导航链接将获得一个类似“查看 2003 年 12 月”的标题。

    使用标题可以提高 Web 窗体的可访问性。许多浏览器在工具提示中呈现 title 信息,但专门的浏览器可能会以语音或盲文形式呈现。

    警告:虽然日期、月份名称和日期是根据当前区域性生成的,但其余标题文本是英文的。

  • MaxVisibleDate (DateTime)

    设置用户可以使用下一个月导航链接导航到的月份的上限。当当前查看的月份与 MaxVisibleDate 相同时,下一个月导航链接将从显示中省略。

  • MinVisibleDate (DateTime)

    设置用户可以使用下一个月导航链接导航到的月份的下限。当当前查看的月份与 MinVisibleDate 相同时,上一个月的导航链接将从显示中省略。

请注意,MixVisibleDateMaxVisibleDate 都默认为 System.DateTime.MinValue,这表示未设置限制。

设计时支持

BetterCalendar 包含用于 Visual Studio .NET 设计时支持的元数据,因此在设计视图中编辑控件实例时,新属性将出现在“属性”窗口中。

虽然这不是使用控件所必需的,但源代码中包含一个名为 BrainJar.xsd 的文件,可用于在 HTML 视图中编辑控件时删除那些令人尴尬的红色波浪线。

要使用模式,请将 BrainJar.xsd 复制到您的 vs_install_directory\Common7\Packages\schemas\xml 目录(其中 vs_install_directory 是 Visual Studio .NET 的安装目录,通常是 C:\Program Files\Microsoft Visual Studio .NET)。然后,在 Web 窗体的 BODY 标记中添加一个声明

<body xmlns:BrainJar="urn:http://schemas.brainjar.com/AspNet/WebControls">

要在编辑代码隐藏时添加对控件的 Intellisense 支持,您可以将项目源(在 \debug 目录中)中的 BrainJar.Web.UI.WebControls.xml 文件复制到项目的 \bin 目录中。

演示项目

BetterCalendar 控件的几个功能可以在演示项目中看到。该项目包含一个带有常规 Calendar 控件和 BetterCalendar 控件的单个 Web 窗体。

两个日历都只使用外部样式表(Styles.css 文件)中定义的 CSS 类进行样式设置。周末、当前月份之外的日期、选定的日期等都使用不同的样式。您可以立即看到 Calendar 控件如何未能正确处理样式类。

在代码隐藏中,BetterCalendar 控件被初始化,将 MinVisibleDateMaxVisibleDate 分别设置为当前日期之前和之后的 90 天。

private void Page_Load(object sender, System.EventArgs e)
{
  // On the initial load, set the min and max view date to 90 days
  // before and after today's date, respectively.
  if (!this.IsPostBack)
  {
    this.BetterCalendar1.MinVisibleDate = DateTime.Today.AddDays(-90);
    this.BetterCalendar1.MaxVisibleDate = DateTime.Today.AddDays(+90);
  }
}

如果您翻阅 BetterCalendar 控件上的月份,您会注意到当您达到相应的日期限制时,下一个月或上一个月的导航链接将消失。

对于两个日历,都设置了一个 DayRender 事件处理程序。在其中,我们将该日的日期与相同的日期范围进行比较。超出范围的日期的 IsSelectable 属性设置为 false

private void Calendar_DayRender(object sender,
  System.Web.UI.WebControls.DayRenderEventArgs e)
{
  // Make days outside the view range nonselectable.
  if ((this.BetterCalendar1.MinVisibleDate != DateTime.MinValue &&
       e.Day.Date < this.BetterCalendar1.MinVisibleDate) ||
      (this.BetterCalendar1.MaxVisibleDate != DateTime.MinValue &&
       e.Day.Date > this.BetterCalendar1.MaxVisibleDate))
    e.Day.IsSelectable = false;
}

通过导航到日期范围两端的月份,我们可以看到两个日历都包含相同的不可选日期。现在,尝试单击月份选择器(日期标题旁边的“>>”链接)。Calendar 控件会选择月份中的每一天,即使是我们明确在 DayRender 事件处理程序中标记为不可选的日期。

但是,如果您在 BetterCalendar 控件上选择相同的月份,您会看到标记为不可选的日期被排除。这同样适用于包含不可选日期的周。

如果您希望 BetterCalender 在周和月选择方面与 Calendar 行为相同,只需将控件的 SelectAllInRange 属性设置为 true

关注点

控件渲染

BetterCalendar 的大部分代码都用于重写的 Render 方法。Calendar 控件生成了相当多的 HTML,并且有几个显示选项,例如 NextPrevFormatNextMonthTextPrevMonthTextShowTitleTitleFormatShowDaysHeaderFirstDayOfWeek 等,需要考虑。因此,为了可读性,一些工作被分解成独立的函数。

在大多数情况下,控件是使用 System.Web.UI.WebControls 中的现有控件构建的,而不是通过编写原始 HTML。使用这些控件固有的属性和方法来分配属性和应用样式比手动将其编写为 HTML 要容易得多。

值检查(或不检查)

当设置 MinVisibleDateMaxVisibleDateVisibleDate 属性时,不进行值检查。如果任何一个属性被赋予的值使 VisibleDate 小于 MinVisibleDate 或大于 MaxVisibleDate,则添加代码以抛出 System.ArgumentOutOfRangeException 会相当容易。

但是,即使 VisibleDate 超出隐含范围,它也不会在控件内引起任何问题。因此,与其抛出异常,不如由应用程序程序员确保这些属性的一致性。这为您提供了一些灵活性,因为您不必担心以任何特定顺序设置这些属性。

回发事件

Calendar 控件使用回发链接来处理下一个月和上一个月的导航以及日期选择。在浏览器中查看日历时,您可以将鼠标悬停在日期链接上并看到类似

javascript:__doPostBack('Calendar1','1646')

的 URI。'Calendar1' 是控件 ID,显然,'1646' 某种程度上指的是一个日期。经过一些猜测和实验,结果发现这些值表示给定日期和 2000 年 1 月 1 日之间的天数。

为了在 BetterCalendar 中使用相同的方案,定义了一个 DateTime 常量和两个方法

private static readonly DateTime DayCountBaseDate = new DateTime(2000, 1, 1);

//
// Returns the number of days between the given DateTime value and the
// base date.
//
private int DayCountFromDate(DateTime date)
{
  return ((TimeSpan) (date - BetterCalendar.DayCountBaseDate)).Days;
}

//
// Returns a DateTime value equal to the base date plus the given number
// of days.
//
private DateTime DateFromDayCount(int dayCount)
{
  return BetterCalendar.DayCountBaseDate.AddDays(dayCount);
}

对于下一个月和上一个月的返回链接,参数以字母“V”为前缀

javascript:__doPostBack('Calendar1','V1613')

这意味着“将可见月份更改为 2004 年 6 月 1 日”。

对于选择一周或整个月份的回发链接,参数以字母“R”开头,后跟天数,最后是表示要选择的总天数的两位数字

javascript:__doPostBack('Calendar1','R164607')

这意味着“从 2004 年 7 月 4 日开始选择连续七天”。

BetterCalendar 中实现的 RaisePostBackEvent 事件处理解析此参数并执行适当的操作(更改可见月份或选择日期或日期范围)。

存储不可选日期

BetterCalendar.Render 方法中,为日历显示中的每一天创建一个 System.Web.UI.WebControls.CalendarDay 和一个 System.Web.UI.WebControls.TableCell 对象。引发 OnDayRender 事件,并将这两个对象传递给分配给控件的任何 DayRender 事件处理程序,就像 Calendar 所做的那样。

控件从事件处理程序返回后,会检查 CalendarDay.IsSelectable 属性。如果它设置为 false,则该日的日期将添加到 ArrayList 中。

// Create a list for storing nonselectable dates.
ArrayList nonselectableDates = new ArrayList();

for (...)

  for (...)

    ...

    // Create a CalendarDay and a TableCell for the date.
    CalendarDay day = this.Day(date);
    TableCell cell = this.Cell(day);

    // Raise the OnDayRender event.
    this.OnDayRender(cell, day);

    // If the day was marked nonselectable, add it to the list.
    if (!day.IsSelectable)
      nonselectableDates.Add(day.Date.ToShortDateString());

显而易见的做法是将此列表保存到视图状态。但是,由于控件正在渲染,这将不起作用,因为控件的 SaveViewState 方法已经调用。

相反,数据存储在添加到页面中的隐藏字段中。定义了几个方法来处理将 ArrayList 中的日期转换为适合存储在隐藏表单字段中并随后恢复的字符串值。

//
// Saves a list of dates to the hidden form field.
//
private void SaveNonselectableDates(ArrayList dates)
{
  // Build a string array by converting each date to a day count
  // value.
  string[] list = new string[dates.Count];
  for (int i = 0; i < list.Length; i++)
    list[i] =
      this.DayCountFromDate(DateTime.Parse(dates[i].ToString())).ToString();

  // Get the hidden field name.
  string fieldName  = this.GetHiddenFieldName();

  // For the field value, create a comma-separated list from the day
  // count values.
  string fieldValue =
    HttpUtility.HtmlAttributeEncode(String.Join(",", list));

  // Add the hidden form field to the page.
  this.Page.RegisterHiddenField(fieldName, fieldValue);
}

//
// Returns a list of dates stored in the hidden form field.
//
private ArrayList LoadNonselectableDates()
{
  // Get the value stored in the hidden form field.
  string fieldName  = this.GetHiddenFieldName();
  string fieldValue = this.Page.Request.Form[fieldName];

  // Extract the individual day count values.
  string[] list = fieldValue.Split(',');

  // Convert those values to dates and store them in an array list.
  ArrayList dates = new ArrayList();
  foreach (string s in list)
    dates.Add(this.DateFromDayCount(Int32.Parse(s)));

return dates;
}

//
// Returns the name of the hidden field used to store nonselectable
// dates on the form.
//
private string GetHiddenFieldName()
{
  // Create a unique field name.
  return String.Format("{0}_NonselectableDates", this.ClientID);
}

RaisePostBackEvent 中,当选择日期范围时,我们可以加载此列表并使用它来删除这些日期。

...

this.SelectedDates.Clear();
this.SelectedDates.SelectRange(date, date.AddDays(n - 1));

// If SelectAllInRange is false, remove any dates found
// in the nonselectable date list.
if (!this.SelectAllInRange)
{
  ArrayList nonselectableDates = this.LoadNonselectableDates();
  foreach(DateTime badDate in nonselectableDates)
    this.SelectedDates.Remove(badDate);
}

历史

  • 2004 年 4 月 19 日
    • 初始版本。
  • 2004 年 5 月 14 日
    • 更新以改进全球化支持。月份和日期名称和缩写、日期格式以及一周的第一天默认值现在基于当前区域性。
  • 2004年5月24日
    • 修复了选择周或月时 SelectionChanged 事件并非总被引发的错误。
© . All rights reserved.