Features

This is an ASP.NET MVC version of the original ASP.NET WebForms Hotel Room Booking tutorial.

  • ASP.NET MVC 5
  • ASP.NET MVC Scheduler control
  • Room status (ready, dirty, cleanup)
  • Reservation status (new, confirmed, arrived, checked out, expired)
  • Drag and drop reservation scheduling
  • Reservation editing using a modal dialog
  • Sample SQL Server database
  • C# and VB version
  • Includes a trial version of DayPilot Pro for ASP.NET MVC

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 MVC. Buy a license.

Scheduler Configuration

asp.net-mvc-hotel-room-booking-configuration.png

The ASP.NET MVC Scheduler control is added to the MVC view using the HTML helper (Html.DayPilotScheduler):

C#

@Html.DayPilotScheduler("dp", new DayPilotSchedulerConfig
{
  // ...
})

VB

@Html.DayPilotScheduler("dp", New DayPilotSchedulerConfig With
{
  ' ...
})

It uses the following configuration:

1. The BackendUrl is set to "Scheduler/Backend". This MVC action is handled by the server-side part of the Scheduler control and it lets you load resources (rooms), events (reservations) and handle the events on the server side:

BackendUrl = Url.Action("Backend", "Scheduler"),

2. Event height is increased to 60 pixels:

EventHeight = 60,

3. Event click handling is set to JavaScript. The custom JavaScript event handler opens a modal dialog with reservation details:

EventClickHandling = EventClickHandlingType.JavaScript,
EventClickJavaScript = "edit(e);",

4. Selecting a time range is mapped to a modal dialog that will let the users enter new reservation details:

TimeRangeSelectedHandling = TimeRangeSelectedHandlingType.JavaScript,
TimeRangeSelectedJavaScript = "create(start, end, resource);",

5. Drag and drop reservation moving and resizing events are handled on the server side using an AJAX callback:

EventMoveHandling = EventMoveHandlingType.CallBack,
EventResizeHandling = EventResizeHandlingType.CallBack,

6. The Scheduler scale (cell duration) is set to "Day". 

Scale = TimeScale.Day,

7. The time headers will display months in the first row and days in the second row:

TimeHeaders = new TimeHeaderCollection()
{
  new TimeHeader(GroupBy.Month),
  new TimeHeader(GroupBy.Day)
},

8. The row headers will display three columns (Room, Size, Status):

HeaderColumns = new RowHeaderColumnCollection()
{
  new RowHeaderColumn("Room", 80),
  new RowHeaderColumn("Size", 80),
  new RowHeaderColumn("Status", 80)
}

The complete Scheduler configuration:

C#

@Html.DayPilotScheduler("dp", new DayPilotSchedulerConfig
{
    BackendUrl = Url.Action("Backend", "Scheduler"),
    
    EventHeight = 60,
    
    TimeRangeSelectedHandling = TimeRangeSelectedHandlingType.JavaScript,
    TimeRangeSelectedJavaScript = "create(start, end, resource);",
    
    EventClickHandling = EventClickHandlingType.JavaScript,
    EventClickJavaScript = "edit(e);",
    
    EventMoveHandling = EventMoveHandlingType.CallBack,
    EventResizeHandling = EventResizeHandlingType.CallBack,
    
    Scale = TimeScale.Day,
    TimeHeaders = new TimeHeaderCollection()
    {
        new TimeHeader(GroupBy.Month),
        new TimeHeader(GroupBy.Day)
    },

    HeaderColumns = new RowHeaderColumnCollection()
    {
        new RowHeaderColumn("Room", 80),
        new RowHeaderColumn("Size", 80),
        new RowHeaderColumn("Status", 80)
    }
})

VB

@Html.DayPilotScheduler("dp", New DayPilotSchedulerConfig With
{
    .BackendUrl = Url.Action("Backend", "Scheduler"),
    .Scale = TimeScale.Day,    
    .EventHeight = 60,
    .TimeRangeSelectedHandling = TimeRangeSelectedHandlingType.JavaScript,
    .TimeRangeSelectedJavaScript = "create(start, end, resource);",
    .EventClickHandling = EventClickHandlingType.JavaScript,
    .EventClickJavaScript = "edit(e);",    
    .EventMoveHandling = EventMoveHandlingType.CallBack,
    .EventResizeHandling = EventResizeHandlingType.CallBack,    
    .TimeHeaders = New TimeHeaderCollection()  From  {
        New TimeHeader(GroupBy.Month),
        New TimeHeader(GroupBy.Day)
    },
    .HeaderColumns = new RowHeaderColumnCollection()  From  {
        New RowHeaderColumn("Room", 80),
        New RowHeaderColumn("Size", 80),
        New RowHeaderColumn("Status", 80)
    }
})

Database Schema (SQL Server)

asp.net-mvc-hotel-room-booking-database-schema.png

This sample ASP.NET MVC application uses a simple SQL Server database with two tables:

[Reservation] table

CREATE TABLE [dbo].[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
);

[Room] table

CREATE TABLE [dbo].[Room] (
  [RoomId]     INT            IDENTITY (1, 1) NOT NULL,
  [RoomName]   NVARCHAR (100) NOT NULL,
  [RoomStatus] NVARCHAR (10)  NOT NULL,
  [RoomSize]   INT            NOT NULL
);

Room Status

asp.net-mvc-hotel-room-booking-room-status.png

The row header data (rooms) and additional details (size, status) is specified in LoadRooms() method:

C#

private void LoadRooms()
{
  Resources.Clear();

  DataTable dt = Db.GetRooms();

  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));

      Resources.Add(res);
  }
}

VB

Private Sub LoadRooms()
  Resources.Clear()

  Dim dt As DataTable = Db.GetRooms()

  For Each r As DataRow In dt.Rows
    Dim name As String = DirectCast(r("RoomName"), String)
    Dim id As String = Convert.ToString(r("RoomId"))
    Dim status As String = DirectCast(r("RoomStatus"), String)
    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)
    res.DataItem = r
    res.Columns.Add(New ResourceColumn(bedsFormatted))
    res.Columns.Add(New ResourceColumn(status))

    Resources.Add(res)
  Next r
End Sub

The row header cells are customized using BeforeResHeaderRender event handler. It adds a special CSS class depending on the room status:

C#

protected override void OnBeforeResHeaderRender(BeforeResHeaderRenderArgs 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 Overrides Sub OnBeforeResHeaderRender(ByVal e As BeforeResHeaderRenderArgs)
  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

Reservation Status

asp.net-mvc-hotel-room-booking-reservation-status.png

The event (reservation) boxes are customized using BeforeEventRender event handler.

  • It uses a custom duration bar color depending on the reservation status (gray, yellow, blue, etc.).
  • It displays the reservation status as text ("New", "Arrived"...)
  • It shows how much has been paid already ("50%").

C#

protected override void OnBeforeEventRender(BeforeEventRenderArgs e)
{
  e.Html = 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.");
  }

  e.Html = e.Html + String.Format("<br /><span style='color:gray'>{0}</span>", e.ToolTip);


  int paid = Convert.ToInt32(e.DataItem["ReservationPaid"]);
  string paidColor = "#aaaaaa";

  e.Areas.Add(new Area().Bottom(10).Right(4).Html("<div style='color:" + paidColor + "; font-size: 8pt;'>Paid: " + paid + "%</div>").Visible());
  e.Areas.Add(new Area().Left(4).Bottom(8).Right(4).Height(2).Html("<div style='background-color:" + paidColor + "; height: 100%; width:" + paid + "%'></div>").Visible());
}

VB

Protected Overrides Sub OnBeforeEventRender(ByVal e As BeforeEventRenderArgs)
  e.Html = 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

  e.Html = e.Html & String.Format("<br /><span style='color:gray'>{0}</span>", e.ToolTip)


  Dim paid As Integer = Convert.ToInt32(e.DataItem("ReservationPaid"))
  Dim paidColor As String = "#aaaaaa"

  e.Areas.Add((New Area()).Bottom(10).Right(4).Html("<div style='color:" & paidColor & "; font-size: 8pt;'>Paid: " & paid & "%</div>").Visible())
  e.Areas.Add((New Area()).Left(4).Bottom(8).Right(4).Height(2).Html("<div style='background-color:" & paidColor & "; height: 100%; width:" & paid & "%'></div>").Visible())
End Sub

Reservation Moving Rules

asp.net-mvc-hotel-room-booking-moving.png

When the users moves a reservation to the new position it is verified using a set of rules on the server side:

  • The reservation can't overlap with another reservation
  • It's not possible to move old reservations
  • It's not possible to move reservation to the past

If the move operation is denied the user will be informed in the integrated message bar using UpdateWithMessage() method.

C#

protected override void OnEventMove(EventMoveArgs e)
{
  string id = e.Id;
  DateTime start = e.NewStart;
  DateTime end = e.NewEnd;
  string resource = e.NewResource;

  string message = null;
  if (!Db.IsFree(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.NewStart < DateTime.Today)
  {
      message = "The reservation cannot be moved to the past.";
  }
  else
  {
      Db.MoveReservation(e.Id, e.NewStart, e.NewEnd, e.NewResource);
  }
  
  LoadReservations();
  UpdateWithMessage(message);
}

VB

Protected Overrides Sub OnEventMove(ByVal e As EventMoveArgs)
  Dim id As String = e.Id
  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 Db.IsFree(id, 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.NewStart < Date.Today Then
    message = "The reservation cannot be moved to the past."
  Else
    Db.MoveReservation(e.Id, e.NewStart, e.NewEnd, e.NewResource)
  End If

  LoadReservations()
  UpdateWithMessage(message)
End Sub

Room Filtering

asp.net-mvc-hotel-room-booking-room-filter.png

The rooms can be filtered using a simple drop-down list above the Scheduler.

The selected filter value is stored in the ClientState and a refresh of the Scheduler is requested.

C#

<div style="margin-bottom: 20px;">    
    Show rooms:
    @Html.DropDownList("Filter", new SelectListItem[]
        { 
            new SelectListItem() { Text = "All", Value = "0" },
            new SelectListItem() { Text = "Single", Value = "1" },
            new SelectListItem() { Text = "Double", Value = "2" },
            new SelectListItem() { Text = "Triple", Value = "3" },
            new SelectListItem() { Text = "Family", Value = "4" },
        },
        new { @onchange = "filter('room', this.value)" }
    )
</div>

...

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

VB

<div style="margin-bottom: 20px;">    
    Show rooms:
    @Html.DropDownList("Filter", New SelectListItem() { 
            New SelectListItem() With { .Text = "All", .Value = "0" },
            New SelectListItem() With { .Text = "Single", .Value = "1" },
            New SelectListItem() With { .Text = "Double", .Value = "2" },
            New SelectListItem() With { .Text = "Triple", .Value = "3" },
            New SelectListItem() With { .Text = "Family", .Value = "4" }
        },
        New With { Key .onchange = "filter('room', this.value)" }
    )
</div>

...

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

The value of the current filter is used when loading the room list from the database using LoadRooms() method.

C#

protected override void OnCommand(CommandArgs e)
{
  switch (e.Command)
  {
    // ...
    case "filter":
      LoadRoomsAndReservations();
      UpdateWithMessage("Updated", CallBackUpdateType.Full);
      break;
  }
}

private void LoadRoomsAndReservations()
{
  LoadRooms();
  LoadReservations();
}

private void LoadRooms()
{
  Resources.Clear();

  string roomFilter = "0";
  if (ClientState["filter"] != null)
  {
      roomFilter = (string)ClientState["filter"]["room"];
  }

  DataTable dt = Db.GetRoomsFiltered(roomFilter);

  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));

      Resources.Add(res);
  }
}

private void LoadReservations()
{
  Events = Db.GetReservations().Rows;

  DataStartField = "ReservationStart";
  DataEndField = "ReservationEnd";
  DataIdField = "ReservationId";
  DataTextField = "ReservationName";
  DataResourceField = "RoomId";

  DataTagFields = "ReservationStatus";

}

VB

Protected Overrides Sub OnCommand(ByVal e As CommandArgs)
  Select Case e.Command
    Case "filter"
      LoadRoomsAndReservations()
      UpdateWithMessage("Updated", CallBackUpdateType.Full)
  End Select
End Sub

Private Sub LoadRoomsAndReservations()
  LoadRooms()
  LoadReservations()
End Sub

Private Sub LoadReservations()
  Events = Db.GetReservations().Rows

  DataStartField = "ReservationStart"
  DataEndField = "ReservationEnd"
  DataIdField = "ReservationId"
  DataTextField = "ReservationName"
  DataResourceField = "RoomId"

  DataTagFields = "ReservationStatus"

End Sub

Private Sub LoadRooms()
  Resources.Clear()

  Dim roomFilter As String = "0"
  If ClientState("filter") IsNot Nothing Then
    roomFilter = CStr(ClientState("filter")("room"))
  End If

  Dim dt As DataTable = Db.GetRoomsFiltered(roomFilter)

  For Each r As DataRow In dt.Rows
    Dim name As String = DirectCast(r("RoomName"), String)
    Dim id As String = Convert.ToString(r("RoomId"))
    Dim status As String = DirectCast(r("RoomStatus"), String)
    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)
    res.DataItem = r
    res.Columns.Add(New ResourceColumn(bedsFormatted))
    res.Columns.Add(New ResourceColumn(status))

    Resources.Add(res)
  Next r
End Sub

Check-In/Check-Out Time

If you use Scale = TimeScale.Day the day cells will begin at 00:00 and end at 24:00 each day.

asp.net-mvc-hotel-checkin-midnight.png

To use a custom check-in/check-out time you can set the Scheduler to use a custom timeline. In this example the check-in and the check-out time are both set to 12:00 (noon).

asp.net-mvc-hotel-checkin-noon.png

MVC View

@Html.DayPilotScheduler("dp", new DayPilotSchedulerConfig
{
  Scale = TimeScale.Manual,
  // ...
})

Set the bottom time header row to be grouped by day (GroupBy.Day). 

The default row header group mode aligns the header cells with the grid cells (GroupBy.Cell).

@Html.DayPilotScheduler("dp", new DayPilotSchedulerConfig
{
  Scale = TimeScale.Manual,
  // ...
  TimeHeaders = new TimeHeaderCollection()
  {
      new TimeHeader(GroupBy.Month),
      new TimeHeader(GroupBy.Day)
  },  
  // ...
})

MVC Controller

protected override void OnInit(InitArgs e)
{
  DateTime start = new DateTime(2015, 1, 1, 12, 0, 0);
  DateTime end = new DateTime(2016, 1, 1, 12, 0, 0);

  Timeline = new TimeCellCollection();
  for (DateTime cell = start; cell < end; cell = cell.AddDays(1))
  {
      Timeline.Add(cell, cell.AddDays(1));
  }

  // ...
}