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
  • Visual Studio 2017 project
  • 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.

MVC 5 Scheduler Configuration

asp.net mvc hotel room booking configuration

You can add the ASP.NET MVC Scheduler control 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 room 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)
}

Here is 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

This hotel room reservation 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

The row header data (rooms) and additional details (size, status) are specified using 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

We will customize the row header cells 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

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

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

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

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

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

  // ...
}