Features
This tutorial shows how to create a visual hotel room booking application in ASP.NET MVC.
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 project
C# and VB version
There is also an ASP.NET Core version of this tutorial available:
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.
MVC 5 Scheduler 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)
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
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
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
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
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.
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).
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));
}
// ...
}