1. 前言
本文探讨的是基于DayPilot Calendar 控件实现的复杂网页的拖拽式交互。 根据以往的经验, 对于元素很多的页面,不应该滥用UpdatePanel来进行Ajax的页面异步更新,考虑采用其他方法解决。首先考虑ASP.NET AJAX库, 在javascript 这一端的开发虽然有点不太习惯,但是尚可接受, 可就是开发服务器端Callback接口的时候只提供了两个选择,要么static Page Method, 要么实现Webservice 接口; 笔者不准备用Webservice 接口做实验, 可使用static 的页面方法的话开发实在是太受限(不知道微软怎么想的,据网上说是由于设计成普通的页面方法调的时候好像性能太差才这么做),于是只好转而使用.net自带的ICallbackEventHandler接口, 而static PageMethod的方式只用来完成其中的一点点工作。这是其一。 其二, 关键的Drag&Drop操作,本来以前使用YahooUI库实现的, 不错, 但这次由于DayPilot 4.3已经加入了从External element拖拽到Calendar中的功能,还不用自己写在Calendar Table中的鼠标定位功能,可以大大减少开发量,于是笔者就采用了它自带的这个js框架,只是在做从Calendar控件中把Event 拖出去的时候才对其js源代码做了一些修改。 最后, 那些自定义的Calendar服务器端回调事件, 都使用DayPilot框架中的Event Delegate在WebPage中实现,由DayPilot中的ICallbackEventHandler.RaiseCallbackEvent统一调度。
实验环境:VS2005, .net2.0, DayPilot 4.3, ASP.NET Ajax库 (MS Ajax Toolkit1.0)
2. DayPilot控件简介
该控件是一个专门的网页日历控件,集成了诸如Resource booking, resource overview, time scheduling, weekly/daily event view 等的功能,获得源代码之后可以对其进行修改,订制出用户想要的ResourceBooking / WebOrdering 系统。 本实验使用的是其最新的4.3版。

3. 待实现的功能
- Room resource calendar,会议室预订日历,横轴是时间, 纵轴为所有的room
- 多选/单选格子生成booking(上图中的蓝色方块), 可以resizing, 可以moving, 可以clicking, 异步刷新Calendar
- 每次进入该room resource calendar页面,装载当天的那些booking, 并且在正上方显示时间,时间两旁有两个按钮,分别进行前一天和后一天的翻页, 异步刷新Calendar
- calendar左边列出所有的公司的员工列表(可以根据输入的字符串自动搜素人名),可以拖拽人名的div块进入calendar,生成一个booking, 异步刷新员工列表,异步刷新Calendar
- 可以将在Calendar中的booking 拖如右边的红色方框的临时区(Temp Area), 并且可以把临时区中的booking 拖回来, 异步刷新临时区, 异步刷新Calendar
4. 技术实现
4.1建立测试网站
VS2005建立ASP.NET Ajax测试网站, 注意配置web.config文件, bin文件夹引入DayPilot.dll, 在<system.web>
-> <pages> -> <controls>中引入<add tagPrefix="dp" namespace="DayPilot.Web.Ui" assembly="DayPilot" />
4.2 数据提供层(弱化)
本实验使用了一个弱化的数据层,一个GenDataHelper类来生成一些测试数据:room resources, original bookings, 以及user list, 代码如下所示:
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Text;


/**//// <summary>
/// Summary description for GenDataHelper
/// </summary>
public class GenDataHelper

...{

-- The simulate data source for Page 'BookingPage' --#region -- The simulate data source for Page 'BookingPage' --
public static DataTable GetColumns()

...{
DataTable dt = new DataTable();
dt.Columns.Add("ColumnName", typeof(string));
dt.Columns.Add("ColumnID", typeof(int));

DataRow dr;
dr = dt.NewRow();
dr["ColumnName"] = "RoomA";
dr["ColumnID"] = 1;
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["ColumnName"] = "RoomB";
dr["ColumnID"] = 2;
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["ColumnName"] = "RoomC";
dr["ColumnID"] = 3;
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["ColumnName"] = "RoomD";
dr["ColumnID"] = 4;
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["ColumnName"] = "RoomE";
dr["ColumnID"] = 5;
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["ColumnName"] = "RoomF";
dr["ColumnID"] = 6;
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["ColumnName"] = "RoomG";
dr["ColumnID"] = 7;
dt.Rows.Add(dr);

return dt;
}

public static DataTable GetOriginalEvents()

...{
DataTable dt = new DataTable();
dt.Columns.Add("StartTime", typeof(DateTime));
dt.Columns.Add("EndTime", typeof(DateTime));
dt.Columns.Add("Name", typeof(string));
dt.Columns.Add("ID", typeof(string));

dt.Columns.Add("ResID", typeof(string)); /**//// the Column's id
dt.Columns.Add("AllDay", typeof(bool));
dt.Columns.Add("IsTemp", typeof(bool));

dt.PrimaryKey = new DataColumn[] ...{ dt.Columns["ID"] };

DataRow dr;
dr = dt.NewRow();
dr["StartTime"] = DateTime.Now.Date.AddHours(12); // Convert.ToDateTime("12:00");
dr["EndTime"] = DateTime.Now.Date.AddHours(13); // Convert.ToDateTime("13:00");
dr["Name"] = "Booking 1";
dr["ID"] = Guid.NewGuid().ToString();
dr["ResID"] = "3";
dr["IsTemp"] = false;
//dr["AllDay"] = true;
dt.Rows.Add(dr);

dr = dt.NewRow();
dr["StartTime"] = DateTime.Now.Date.AddHours(9); // Convert.ToDateTime("9:00");
dr["EndTime"] = DateTime.Now.Date.AddHours(14).AddMinutes(30); // Convert.ToDateTime("14:30");
dr["Name"] = "Booking 2";
dr["ID"] = Guid.NewGuid().ToString();
dr["ResID"] = "7";
dr["IsTemp"] = false;
//dr["AllDay"] = true;
dt.Rows.Add(dr);

return dt;
}

public static DataTable GetInitUserList()

...{
DataTable dt = new DataTable();
dt.Columns.Add("UserID", typeof(int));
dt.Columns.Add("UserName", typeof(string));

for (int i = 1; i <= 20; i++)

...{
DataRow dr = dt.NewRow();
dr["UserID"] = i;
dr["UserName"] = "UserName" + i.ToString();
dt.Rows.Add(dr);
}

return dt;
}

#endregion -- The simulate data source for Page 'BookingPage' --

4.3 界面设计
一个Master page, 左右两个asp:ContentPlaceHolder, 注意要在此处引入ScriptManager
如下:
<asp:ScriptManager id="scriptMgr" runat="server" EnablePageMethods="true">
<Scripts>
<asp:ScriptReference Name="PreviewScript.js" Assembly="Microsoft.Web.Preview" />
</Scripts>
</asp:ScriptManager>
测试页面BookingPage.aspx, 左边的asp:Content放置一个ul用来显示雇员名称列表,右边的asp:Content放置DayPilot控件以及临时区,如下所示:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="BookingPage.aspx.cs" Inherits="BookingPage" MasterPageFile="~/Main.master" %>
<%@ MasterType VirtualPath="~/Main.master" %>

<asp:Content ID="leftContent" runat="server" ContentPlaceHolderID="leftContentHolder">
<asp:Literal ID="literalName" runat="server" Text="UserName"></asp:Literal>
<asp:TextBox ID="txtSearchUserName" BorderColor="black" BorderWidth="1px" runat="server"></asp:TextBox>
<div id="lodingIndicator" style="display:none;color:White;background-color:Red; width:30px;">Loding...</div>
<br />
<div id="leftList-div">
<ul id="leftList"></ul>
</div>
</asp:Content>

<asp:Content ID="rightContent" runat="server" ContentPlaceHolderID="rightContentHolder">
<div style="float:left;">
<div style="float:left; width:80%">
<table style="width:100%;">
<tr>
<td style="text-align:center;">
<div style="text-align:center; margin-bottom:10px; margin-top:10px;">
<a href="javascript:AjaxCall_PrevDay(); " style="text-decoration: none; background-color:White; border: 1px solid navy; padding:3px;">Previous day <span style="color:red">«</span> </a>
<span id="lblCurrentDay" runat="server"></span>
<a href="javascript:AjaxCall_NextDay();" style="text-decoration: none; background-color:White; border: 1px solid navy; padding:3px;">Next day <span style="color:red">»</span> </a>
</div>
</td>
</tr>
<tr>
<td width="80%">
<dp:DayPilotCalendar ID="mainCalendar" runat="server" ClientObjectName="dpc1"
DataEndField="EndTime"
DataStartField="StartTime"
DataTextField="Name"
DataValueField="ID"
DataColumnField="ResID"
DataAllDayField="AllDay"
HourWidth="100"
BusinessBeginsHour="10"
BusinessEndsHour="17"
Days="7"
CellHeight="15"
ViewType="Resources"
CellsPerHour="4"
TimeFormat="Clock24Hours"
UseEventBoxes="always"
EventClickHandling="javaScript"
EventMoveHandling="CallBack"
EventResizeHandling="CallBack"
TimeRangeSelectedHandling="CallBack"
OnBeforeEventRender="mainCalendar_BeforeEventRender"
OnTimeRangeSelected="mainCalendar_TimeRangeSelected"
OnRefresh="mainCalendar_Refresh"
OnEventReload="mainCalendar_Reload"
OnEventMove="mainCalendar_EventMove"
OnEventResize="mainCalendar_EventResize"
OnEventTemp="mainClendar_EventTemp"
>
</dp:DayPilotCalendar>
</td>
</tr>
</table>
</div>
</div>
<div id="tempAreaEvent" style="font-size: 8pt; width: 50px; cursor: move; font-family: Tahoma; margin-top:200px; display:block;
height: 100px; border:2px solid red; z-index:10000; float:left; vertical-align:middle" unselectable="on"
onmouseup="TempArea_MouseUp();">
</div>
</asp:Content>

4.4 初始化页面,以及装载RoomResources
页面代码完成之后, 由于还没有规定Calendar 的Columns是什么,因此在运行的时候不会画出上面黄色的table cells, 因此需要在后台代码调用数据层类GenDataHelper 获取Columns集合; 在此之前也要做一些必要的初始化工作,具体步骤如下:
- 页面类继承ICallbackEventHander接口, 以处理客户端的异步请求
public partial class BookingPage : System.Web.UI.Page, System.Web.UI.ICallbackEventHandler
- 页面的Page_PreRender事件中注册客户端的Callback方法
protected void Page_PreRender(object sender, EventArgs e)

...{
this.RegClientScript();
}

protected void RegClientScript()

...{
ClientScriptManager cs = Page.ClientScript;
string js = @"function OnPageCallback(argument){" + cs.GetCallbackEventReference(this, "argument", "onShowResult", null) + @";}";
cs.RegisterStartupScript(this.GetType(), "OnPageCallback", js, true);
}
此处的OnPageCallback可以在客户端的js代码中调用,发送异步请求到服务器端, 由ICallbackEventHandler.RaiseCallbackEvent接口进行处理,而onShowResult是代码写在客户端的回调结果处理方法。
- 在OnLoad事件中装载当前日期, Room Resources(columns), 以及original booking events, 雇员列表
protected override void OnLoad(EventArgs e)

...{
base.OnLoad(e);
if (!IsPostBack)

...{
this.mainCalendar.StartDate = DateTime.Now.Date;

CurrentDay = DateTime.Now.Date;

this.LoadTestColumns();

this.LoadTestEvents();
this.ReBindNonTempEvents();

this.SetCurrentDayShow();

this.LoadInitUserList();
}
}

public DateTime CurrentDay

...{
get

...{
if (Session["CurrentDay"] == null)
Session["CurrentDay"] = DateTime.Now.Date;
return (DateTime)Session["CurrentDay"];
}
set

...{
Session["CurrentDay"] = value;
}
}

private void LoadTestColumns()

...{
this.mainCalendar.Columns.Clear();
DataTable dtColumns = GenDataHelper.GetColumns();
if (dtColumns != null)

...{

foreach (DataRow dr in dtColumns.Rows) ...{
this.mainCalendar.Columns.Add(dr["ColumnName"].ToString(), dr["ColumnID"].ToString());
}
}
}

private void LoadTestEvents()

...{
if (Session["Events"] == null