This tutorial shows how to use DayPilot Scheduler to build a hotel room booking application.

Online Demo

Tutorial License

Licensed for testing and evaluation purposes. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for ASP.NET WebForms. Buy a license.

Hotel Reservation Features

  • Colors indicating the reservation phase
  • Room status (dirty, cleanup, ready)
  • Rules for reservation updates (overlap forbidden, past reservations)
  • Filters for visible rooms (based on a drop-down list)
  • Modal dialog for event creating and editing
  • Flash notification about user action result

Requirements

  • .NET Framework 4.0 or higher
  • Visual Studio 2010 or higher (optional)

Database Setup

This tutorial includes a sample SQL Server (2008+) database (daypilot.mdf).

The database contains two tables:

  • Room
  • Reservation

The [Reservation] table stores the events (reservations).

CREATE TABLE [Reservation](
	[ReservationId] [int] IDENTITY(1,1) NOT NULL,
	[RoomId] [int] NOT NULL,
	[ReservationName] [nvarchar](100) NOT NULL,
	[ReservationPaid] [int] NOT NULL,
	[ReservationStatus] [int] NOT NULL,
	[ReservationStart] [datetime] NOT NULL,
	[ReservationEnd] [datetime] NOT NULL
);

The [Room] table stores the room details.

CREATE TABLE [Room](
	[RoomId] [int] IDENTITY(1,1) NOT NULL,
	[RoomName] [nvarchar](100) NOT NULL,
	[RoomStatus] [nvarchar](10) NOT NULL,
	[RoomSize] [int] NOT NULL
);

Room Details and Status

hotel-reservation-asp.net-rooms.png

We will display the rooms on the vertical axis. First, we define the header columns:

<DayPilot:DayPilotScheduler 
  ID="DayPilotScheduler1" 
  runat="server" 
  ...
  >
  <HeaderColumns>
      <DayPilot:RowHeaderColumn Title="Room" Width="80" />
      <DayPilot:RowHeaderColumn Title="Size" Width="80" />
      <DayPilot:RowHeaderColumn Title="Status" Width="80" />
  </HeaderColumns>
</DayPilot:DayPilotScheduler>

Then, we load the resources:

C#

private void LoadResources()
{
  DayPilotScheduler1.Resources.Clear();

  // filtering explained below
  string roomFilter = "0";
  if (DayPilotScheduler1.ClientState["filter"] != null)
  {
      roomFilter = (string) DayPilotScheduler1.ClientState["filter"]["room"];
  }

  SqlDataAdapter da = new SqlDataAdapter("SELECT [RoomId], [RoomName], [RoomStatus], [RoomSize] FROM [Room] WHERE RoomSize = @beds or @beds = '0'", ConfigurationManager.ConnectionStrings["daypilot"].ConnectionString);
  da.SelectCommand.Parameters.AddWithValue("beds", roomFilter);
  DataTable dt = new DataTable();
  da.Fill(dt);

  foreach (DataRow r in dt.Rows)
  {
      string name = (string)r["RoomName"];
      string id = Convert.ToString(r["RoomId"]);
      string status = (string)r["RoomStatus"];
      int beds = Convert.ToInt32(r["RoomSize"]);
      string bedsFormatted = (beds == 1) ? "1 bed" : String.Format("{0} beds", beds);

      Resource res = new Resource(name, id);
      res.DataItem = r;
      res.Columns.Add(new ResourceColumn(bedsFormatted));
      res.Columns.Add(new ResourceColumn(status));

      DayPilotScheduler1.Resources.Add(res);
  }
}

VB

Private Sub LoadResources()
  DayPilotScheduler1.Resources.Clear()

  ' room filtering will be explained below
  Dim roomFilter As String = "0"
  If DayPilotScheduler1.ClientState("filter") IsNot Nothing Then
    roomFilter = CStr(DayPilotScheduler1.ClientState("filter")("room"))
  End If

  Dim da As New SqlDataAdapter("SELECT [RoomId], [RoomName], [RoomStatus], [RoomSize] FROM [Room] WHERE RoomSize = @beds or @beds = '0'", ConfigurationManager.ConnectionStrings("daypilot").ConnectionString)
  da.SelectCommand.Parameters.AddWithValue("beds", roomFilter)
  Dim dt As New DataTable()
  da.Fill(dt)

  For Each r As DataRow In dt.Rows
    Dim name As String = CStr(r("RoomName"))
          Dim id_Renamed As String = Convert.ToString(r("RoomId"))
    Dim status As String = CStr(r("RoomStatus"))
    Dim beds As Integer = Convert.ToInt32(r("RoomSize"))
    Dim bedsFormatted As String = If(beds = 1, "1 bed", String.Format("{0} beds", beds))

    Dim res As New Resource(name, id_Renamed)
    res.DataItem = r
    res.Columns.Add(New ResourceColumn(bedsFormatted))
    res.Columns.Add(New ResourceColumn(status))

    DayPilotScheduler1.Resources.Add(res)
  Next r

End Sub

We will use BeforeResHeaderRender event to define custom row CSS class depending on the room status:

C#

protected void DayPilotScheduler1_BeforeResHeaderRender(object sender, BeforeResHeaderRenderEventArgs e)
{
  string status = (string)e.DataItem["RoomStatus"];
  switch (status)
  {
      case "Dirty":
          e.CssClass = "status_dirty";
          break;
      case "Cleanup":
          e.CssClass = "status_cleanup";
          break;
  }
}

VB

Protected Sub DayPilotScheduler1_BeforeResHeaderRender(ByVal sender As Object, ByVal e As BeforeResHeaderRenderEventArgs)
  Dim status As String = CStr(e.DataItem("RoomStatus"))
  Select Case status
    Case "Dirty"
      e.CssClass = "status_dirty"
    Case "Cleanup"
      e.CssClass = "status_cleanup"
  End Select
End Sub

The custom CSS classes will extend the CSS theme (scheduler_white):

.scheduler_white_rowheader 
{
    background: -webkit-gradient(linear, left top, left bottom, from(#eeeeee), to(#dddddd));
  background: -moz-linear-gradient(top, #eeeeee 0%, #dddddd);
  background: -ms-linear-gradient(top, #eeeeee 0%, #dddddd);
  background: -webkit-linear-gradient(top, #eeeeee 0%, #dddddd);
  background: linear-gradient(top, #eeeeee 0%, #dddddd);
  filter: progid:DXImageTransform.Microsoft.gradient(startColorStr="#eeeeee", endColorStr="#dddddd");

}
.scheduler_white_rowheader_inner 
{
  border-right: 1px solid #ccc;
}
.scheduler_white_rowheadercol2
{
  background: White;
}
.scheduler_white_rowheadercol2 .scheduler_white_rowheader_inner 
{
  top: 2px;
  bottom: 2px;
  left: 2px;
  background-color: transparent;
  border-left: 5px solid #1a9d13; /* green */
  border-right: 0px none;
}
.status_dirty.scheduler_white_rowheadercol2 .scheduler_white_rowheader_inner
{
  border-left: 5px solid #ea3624; /* red */
}
.status_cleanup.scheduler_white_rowheadercol2 .scheduler_white_rowheader_inner
{
  border-left: 5px solid #f9ba25; /* orange */
}

Highlighting Today

We will use a separator to insert a red vertical line highlighting the current time.

hotel-reservation-asp.net-today.png

C#

protected void Page_Load(object sender, EventArgs e)
{
  DayPilotScheduler1.Separators.Clear();
  DayPilotScheduler1.Separators.Add(DateTime.Today, Color.Red);
}

VB.NET

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
  DayPilotScheduler1.Separators.Clear()
  DayPilotScheduler1.Separators.Add(Date.Today, Color.Red)
End Sub

Duration Bar Colors Indicating the Reservation Phase

In the sample reservation application, we will use five reservation phases:

  • New reservation (unconfirmed)
  • Confirmed reservation
  • Arrived (the guest has checked in)
  • Checked out
  • Problem

Each phase will use a specific DurationBar color.

New Reservation (Orange)

hotel-reservation-asp.net-status-new.png

Confirmed Reservation (Green)

hotel-reservation-asp.net-status-confirmed.png

Arrived (Blue)

hotel-reservation-asp.net-status-arrived.png

Checked Out (Gray)

hotel-reservation-asp.net-status-checked-out.png

Problem (Red)

hotel-reservation-asp.net-status-expired.png

The Red color is used for reservation with a problem:

  • New reservations not confirmed 2 days before the start day
  • Confirmed reservations not checked in before 6pm on the start day
  • Stays not checked out before 10 am on the end day

The color is assigned using BeforeEventRender event handler.

  • Note that the "status" database field is used to determine the phase
  • The status is loaded into the Tag collection using DataTagFields="status" declaration
  • In the event handlers, it's available as e.Tag["status"] in the event handler

C#

protected void DayPilotScheduler1_BeforeEventRender(object sender, DayPilot.Web.Ui.Events.Scheduler.BeforeEventRenderEventArgs e)
{
  e.InnerHTML = String.Format("{0} ({1:d} - {2:d})", e.Text, e.Start, e.End);
  int status = Convert.ToInt32(e.Tag["ReservationStatus"]);
  
  switch (status)
  {
      case 0: // new
          if (e.Start < DateTime.Today.AddDays(2)) // must be confirmed two day in advance
          {
              e.DurationBarColor = "red"; 
              e.ToolTip = "Expired (not confirmed in time)";
          }
          else
          {
              e.DurationBarColor = "orange";
              e.ToolTip = "New";
          }
          break;
      case 1:  // confirmed
          if (e.Start < DateTime.Today || (e.Start == DateTime.Today && DateTime.Now.TimeOfDay.Hours > 18))  // must arrive before 6 pm
          {
              e.DurationBarColor = "#f41616";  // red
              e.ToolTip = "Late arrival";
          }
          else
          {
              e.DurationBarColor = "green";
              e.ToolTip = "Confirmed";
          }
          break;
      case 2: // arrived
          if (e.End < DateTime.Today || (e.End == DateTime.Today && DateTime.Now.TimeOfDay.Hours > 11))  // must checkout before 10 am
          {
              e.DurationBarColor = "#f41616"; // red
              e.ToolTip = "Late checkout";
          }
          else
          {
              e.DurationBarColor = "#1691f4";  // blue
              e.ToolTip = "Arrived";
          }
          break;
      case 3: // checked out
          e.DurationBarColor = "gray";
          e.ToolTip = "Checked out";
          break;
      default:
          throw new ArgumentException("Unexpected status.");
  }
}

VB

Protected Sub DayPilotScheduler1_BeforeEventRender(ByVal sender As Object, ByVal e As DayPilot.Web.Ui.Events.Scheduler.BeforeEventRenderEventArgs)
  e.InnerHTML = String.Format("{0} ({1:d} - {2:d})", e.Text, e.Start, e.End)
  Dim status As Integer = Convert.ToInt32(e.Tag("ReservationStatus"))

  Select Case status
    Case 0 ' new
      If e.Start < Date.Today.AddDays(2) Then ' must be confirmed two day in advance
        e.DurationBarColor = "red"
        e.ToolTip = "Expired (not confirmed in time)"
      Else
        e.DurationBarColor = "orange"
        e.ToolTip = "New"
      End If
    Case 1 ' confirmed
      If e.Start < Date.Today OrElse (e.Start = Date.Today AndAlso Date.Now.TimeOfDay.Hours > 18) Then ' must arrive before 6 pm
        e.DurationBarColor = "#f41616" ' red
        e.ToolTip = "Late arrival"
      Else
        e.DurationBarColor = "green"
        e.ToolTip = "Confirmed"
      End If
    Case 2 ' arrived
      If e.End < Date.Today OrElse (e.End = Date.Today AndAlso Date.Now.TimeOfDay.Hours > 11) Then ' must checkout before 10 am
        e.DurationBarColor = "#f41616" ' red
        e.ToolTip = "Late checkout"
      Else
        e.DurationBarColor = "#1691f4" ' blue
        e.ToolTip = "Arrived"
      End If
    Case 3 ' checked out
      e.DurationBarColor = "gray"
      e.ToolTip = "Checked out"
    Case Else
      Throw New ArgumentException("Unexpected status.")
  End Select
End Sub

Reservation Change Rules

hotel-reservation-asp.net-change.png

The booking logic imposes some restrictions. We will implement the following rules:

  • No room can be booked for two guests at the same time (no overlap allowed).
  • The reservations that already checked in can't be moved to another room.
  • The start of reservations that already checked in can't be changed.
  • The end of reservations that already checked out can't be changed.

The Scheduler handles the drag&drop user actions (move and resize) using EventMove and EventResize event handlers.

EventMove Event Handler

C#

protected void DayPilotScheduler1_EventMove(object sender, DayPilot.Web.Ui.Events.EventMoveEventArgs e)
{
  string id = e.Value;
  DateTime start = e.NewStart;
  DateTime end = e.NewEnd;
  string resource = e.NewResource;

  string message = null;

  if (!dbIsFree(id, start, end, resource))
  {
      message = "The reservation cannot overlap with an existing reservation.";
  }
  else if (e.OldEnd <= DateTime.Today)
  {
      message = "This reservation cannot be changed anymore.";
  }
  else if (e.OldStart < DateTime.Today)
  {
      if (e.OldResource != e.NewResource)
      {
          message = "The room cannot be changed anymore.";
      }
      else
      {
          message = "The reservation start cannot be changed anymore.";
      }
  }
  else if (e.NewStart < DateTime.Today)
  {
      message = "The reservation cannot be moved to the past.";
  }
  else
  {
      dbUpdateEvent(id, start, end, resource);
      //message = "Reservation moved.";
  }

  DayPilotScheduler1.DataSource = dbGetEvents(DayPilotScheduler1.StartDate, DayPilotScheduler1.Days);
  DayPilotScheduler1.DataBind();
  DayPilotScheduler1.UpdateWithMessage(message);
}

VB

Protected Sub DayPilotScheduler1_EventMove(ByVal sender As Object, ByVal e As DayPilot.Web.Ui.Events.EventMoveEventArgs)
      Dim id_Renamed As String = e.Value
  Dim start As Date = e.NewStart
  Dim [end] As Date = e.NewEnd
  Dim resource As String = e.NewResource

  Dim message As String = Nothing

  If Not dbIsFree(id_Renamed, start, [end], resource) Then
    message = "The reservation cannot overlap with an existing reservation."
  ElseIf e.OldEnd <= Date.Today Then
    message = "This reservation cannot be changed anymore."
  ElseIf e.OldStart < Date.Today Then
    If e.OldResource <> e.NewResource Then
      message = "The room cannot be changed anymore."
    Else
      message = "The reservation start cannot be changed anymore."
    End If
  ElseIf e.NewStart < Date.Today Then
    message = "The reservation cannot be moved to the past."
  Else
    dbUpdateEvent(id_Renamed, start, [end], resource)
    'message = "Reservation moved.";
  End If

  DayPilotScheduler1.DataSource = dbGetEvents(DayPilotScheduler1.StartDate, DayPilotScheduler1.Days)
  DayPilotScheduler1.DataBind()
  DayPilotScheduler1.UpdateWithMessage(message)
End Sub

EventResize Event Handler

C#

protected void DayPilotScheduler1_EventResize(object sender, DayPilot.Web.Ui.Events.EventResizeEventArgs e)
{
  string id = e.Value;
  DateTime start = e.NewStart;
  DateTime end = e.NewEnd;
  string resource = e.Resource;

  string message = null;

  if (!dbIsFree(id, start, end, resource))
  {
      message = "The reservation cannot overlap with an existing reservation.";
  }
  else if (e.OldEnd <= DateTime.Today)
  {
      message = "This reservation cannot be changed anymore.";
  }
  else if (e.OldStart != e.NewStart)
  {
      if (e.OldStart < DateTime.Today)
      {
         message = "The reservation start cannot be changed anymore.";
      }
      else if (e.NewStart < DateTime.Today)
      {
          message = "The reservation cannot be moved to the past.";
      }
  }
  else
  {
      dbUpdateEvent(id, start, end, resource);
      //message = "Reservation updated.";
  }

  DayPilotScheduler1.DataSource = dbGetEvents(DayPilotScheduler1.StartDate, DayPilotScheduler1.Days);
  DayPilotScheduler1.DataBind();
  DayPilotScheduler1.UpdateWithMessage(message);
}

VB

Protected Sub DayPilotScheduler1_EventResize(ByVal sender As Object, ByVal e As DayPilot.Web.Ui.Events.EventResizeEventArgs)
      Dim id_Renamed As String = e.Value
  Dim start As Date = e.NewStart
  Dim [end] As Date = e.NewEnd
  Dim resource As String = e.Resource

  Dim message As String = Nothing

  If Not dbIsFree(id_Renamed, start, [end], resource) Then
    message = "The reservation cannot overlap with an existing reservation."
  ElseIf e.OldEnd <= Date.Today Then
    message = "This reservation cannot be changed anymore."
  ElseIf e.OldStart <> e.NewStart Then
    If e.OldStart < Date.Today Then
       message = "The reservation start cannot be changed anymore."
    ElseIf e.NewStart < Date.Today Then
      message = "The reservation cannot be moved to the past."
    End If
  Else
    dbUpdateEvent(id_Renamed, start, [end], resource)
    'message = "Reservation updated.";
  End If

  DayPilotScheduler1.DataSource = dbGetEvents(DayPilotScheduler1.StartDate, DayPilotScheduler1.Days)
  DayPilotScheduler1.DataBind()
  DayPilotScheduler1.UpdateWithMessage(message)
End Sub

Note that the database is updated only when the change meets the rules.

User Notifications

hotel-reservation-asp.net-notification.png

Because the requested change is not allowed in some cases we need to notify the users about the reason. The Scheduler can send a custom message to the client using UpdateWithMessage() method.

DayPilotScheduler1.UpdateWithMessage("The reservation cannot overlap with an existing reservation.");

Room Filtering

hotel-reservation-asp.net-filter.png

We add a DropDown control just above the Scheduler control to enable room filtering.

<asp:DropDownList ID="DropDownListFilter" runat="server" onchange="filter('room', this.value)">
  <asp:ListItem Text="All" Value="0"></asp:ListItem>
  <asp:ListItem Text="Single" Value="1"></asp:ListItem>
  <asp:ListItem Text="Double" Value="2"></asp:ListItem>
  <asp:ListItem Text="Tripple" Value="3"></asp:ListItem>
  <asp:ListItem Text="Family" Value="4"></asp:ListItem>
</asp:DropDownList>

The DropDown value changes are handled using a custom JavaScript on the client side (onchange attribute). This JavaScript updates the scheduler using a fast callback refresh:

 function filter(property, value) {
     if (!dps.clientState.filter) {
         dps.clientState.filter = {};
     }
     if (dps.clientState.filter[property] != value) { // only refresh when the value has changed
         dps.clientState.filter[property] = value;
         dps.commandCallBack('filter');
     }
 }

DayPilot Scheduler has a special client-side property which is sent to the server with every callback (ClientState).

  • The selected value is stored in clientState after every DropDown selection change.
  • We are creating a new filter object but it can be as simple as storing the filter value in the clientState directly. However, then you wouldn't be able to store more complicated filters (state of several controls) in clientState:
function filter(property, value) {
  dps.clientState = value;
  dps.commandCallBack('filter');
}

On the server side, we need to handle Command event and refresh the room and reservation lists:

C#

protected void DayPilotScheduler1_Command(object sender, DayPilot.Web.Ui.Events.CommandEventArgs e)
{
  switch (e.Command)
  {
      case "refresh":
          DayPilotScheduler1.DataSource = dbGetEvents(DayPilotScheduler1.StartDate, DayPilotScheduler1.Days);
          DayPilotScheduler1.DataBind();
          DayPilotScheduler1.Update();
          break;
      case "filter":
          LoadResources();
          DayPilotScheduler1.DataSource = dbGetEvents(DayPilotScheduler1.StartDate, DayPilotScheduler1.Days);
          DayPilotScheduler1.DataBind();
          DayPilotScheduler1.Update();
          break;
  }
}

VB

Protected Sub DayPilotScheduler1_Command(ByVal sender As Object, ByVal e As DayPilot.Web.Ui.Events.CommandEventArgs)
  Select Case e.Command
    Case "refresh"
      DayPilotScheduler1.DataSource = dbGetEvents(DayPilotScheduler1.StartDate, DayPilotScheduler1.Days)
      DayPilotScheduler1.DataBind()
      DayPilotScheduler1.Update()
    Case "filter"
      LoadResources()
      DayPilotScheduler1.DataSource = dbGetEvents(DayPilotScheduler1.StartDate, DayPilotScheduler1.Days)
      DayPilotScheduler1.DataBind()
      DayPilotScheduler1.Update()
  End Select
End Sub

Modal Dialog (Reservation Creating and Editing)

We will use DayPilot.Modal helper to display the modal dialog:

hotel-reservation-asp.net-modal-dialog.png

Usage:

  1. Copy modal.js to js directory.
  2. Copy Modal.cs to App_Code directory.
  3. Include modal.js in Default.aspx: <script type="text/javascript" src="js/modal.js"></script>

The modal dialog is invoked using a short piece of JavaScript:

new DayPilot.Modal().showUrl("Detail.aspx");

The Detail.aspx page needs to call a Modal.Close() in order to close the modal window:

C#

protected void ButtonOK_Click(object sender, EventArgs e)
{
  // do your job here
  Modal.Close(this);
}

VB

Protected Sub ButtonOK_Click(ByVal sender As Object, ByVal e As EventArgs) Handles ButtonOK.Click
    ' do your job here        
    Modal.Close(Me)
End Sub

In order to handle the dialog result we need to use a bit more complex JavaScript initialization:

function createEvent(start, end, resource) {
var modal = new DayPilot.Modal(); modal.border = "10px solid #ccc"; modal.closed = function () { if (this.result == "OK") { dps.commandCallBack('refresh'); } };
  modal.showUrl("New.aspx?start=" + start.toStringSortable() + "&end=" + end.toStringSortable() + "&r=" + resource);
}

The most important command is assigning the .closed handler. This handler will check the result sent from the modal and refresh using commandCallBack if changes were made.

C#

protected void ButtonOK_Click(object sender, EventArgs e)
{
  DateTime start = Convert.ToDateTime(TextBoxStart.Text);
  DateTime end = Convert.ToDateTime(TextBoxEnd.Text);
  string name = TextBoxName.Text;
  string resource = DropDownList1.SelectedValue;

  dbInsertEvent(start, end, name, resource, 0);
  Modal.Close(this, "OK");
}

protected void ButtonCancel_Click(object sender, EventArgs e)
{
  Modal.Close(this);
}

VB

Protected Sub ButtonOK_Click(ByVal sender As Object, ByVal e As EventArgs)
  Dim start As Date = Convert.ToDateTime(TextBoxStart.Text)
  Dim [end] As Date = Convert.ToDateTime(TextBoxEnd.Text)
  Dim name As String = TextBoxName.Text
  Dim resource As String = DropDownList1.SelectedValue

  dbInsertEvent(start, [end], name, resource, 0)
  Modal.Close(Me, "OK")
End Sub

Protected Sub ButtonCancel_Click(ByVal sender As Object, ByVal e As EventArgs)
  Modal.Close(Me)
End Sub

More Tutorials

All tutorials include a sample Visual Studio project with C# and VB.NET source code.