In this tutorial you'll learn how to create a hotel room booking calendar on .NET MVC 5. The created booking app will allow simple and fast room booking in a hotel.
Each booking will contain info about the customer, booking (determines the display color of booking) and payment status, and the associated room. Besides, each room will have its own type and cleanness status. Also, there will be a select box that will filter rooms by type.
The applied technologies are: .NET 4.5, .NET MVC 5, DHTMLX Scheduler.NET, Entity Framework 6.
IDE used in this tutorial: Visual Studio 2015.
Our solution called "HotelRoomBooking" will contain the following assemblies:
We will start implementing our solution from the infrastructure library.
Open Visual Studio, create a solution named "HotelRoomBooking" with the class library "HotelRoomBooking.Infrastructure".
Firstly, declare an interface for repository classes which we'll use to isolate actual implementation of the data access from the rest of application. This project will contain only one item: IRepository interface, let's create it:
public interface IRepository<TEntity>
{
void Create(TEntity entity);
TEntity Read(int id);
void Update(TEntity entity);
void Delete(int id);
Â
IEnumerable<TEntity> ReadWhere(Func<TEntity, bool> predicate);
IEnumerable<TEntity> ReadAll();
}
Next we'll declare classes for our application domain: bookings, rooms, statuses of bookings, statuses of rooms and types of rooms.
Primary entities like Booking and Room will have scheduler mapping data annotations for its properties:
Scheduler.NET event (booking in our case) item contains the mandatory property "text", so we should create a C# class with the "text" lowercase property or create it with the normal naming and map it. We'll use the latter way.
Secondary entities, such as types and statuses, will be implemented by classes (instead of enums) for better extensibility and will be initialized with database creation. Let's create them:
public class BookingStatus
{
public int Id { get; set; }
public string Title { get; set; }
}
public class RoomStatus
{
public int Id { get; set; }
public string Title { get; set; }
}
public class RoomType
{
public int Id { get; set; }
public string Title { get; set; }
}
Install Scheduler.NET via NuGet package manager for data annotations:
PM> Install-Package DHTMLX.Scheduler.NET -ProjectName HotelRoomBooking.Entities
The Scripts folder is worthless in this class library, so it can be deleted. Let's create primary classes:
public class Room
{
[DHXJson(Alias = "key")]
public int Id { get; set; }
[DHXJson(Alias = "room_number")]
public int Number { get; set; }
[DHXJson(Alias = "label")]
public string Label { get; set; }
[DHXJson(Alias = "type")]
public int? RoomTypeId { get; set; }
[DHXJson(Ignore = true)]
public RoomType RoomType { get; set; }
[DHXJson(Alias = "status")]
public int? RoomStatusId { get; set; }
[DHXJson(Ignore = true)]
public RoomStatus RoomStatus { get; set; }
}
public class Booking
{
[DHXJson(Alias = "id")]
public int Id { get; set; }
[DHXJson(Alias = "text")]
public string Name { get; set; }
[DHXJson(Alias = "is_paid")]
public bool IsPaid { get; set; }
[DHXJson(Alias = "start_date")]
public DateTime StartDate { get; set; }
[DHXJson(Alias = "end_date")]
public DateTime EndDate { get; set; }
[DHXJson(Alias = "room_number")]
public int? RoomId { get; set; }
[DHXJson(Ignore = true)]
public virtual Room Room { get; set; }
[DHXJson(Alias = "status")]
public int? BookingStatusId { get; set; }
[DHXJson(Ignore = true)]
public BookingStatus BookingStatus { get; set; }
}
Data Access layer will be implemented with Entity Framework 6. Let's create the class library "HotelRoomBooking.DataAccess.EF" and install the package via NuGet:
Install-Package EntityFramework -ProjectName HotelRoomBooking.DataAccess.EF
Then build HotelRoomBooking.Model project and add a reference to it from HotelRoomBooking.DataAccess.EF:
Create DB context "RoomBookingContext" class:
public class RoomBookingContext : DbContext
{
public DbSet<Room> Rooms { get; set; }
public DbSet<RoomStatus> RoomStatuses { get; set; }
public DbSet<RoomType> RoomTypes { get; set; }
Â
public DbSet<Booking> Bookings { get; set; }
public DbSet<BookingStatus> BookingStatuses { get; set; }
}
Afterwards, create a custom "DbInitializer" class for Entity Framework which will initialize default collections of room and booking statuses, and the room type. Also, the initializer will contain some demo rooms and bookings:
public class DbInitializer : DropCreateDatabaseIfModelChanges<RoomBookingContext>
{
protected override void Seed(RoomBookingContext context)
{
var roomTypes = new List<RoomType>
{
new RoomType {Id = 0, Title = "1 bed"},
new RoomType {Id = 1, Title = "2 beds"},
new RoomType {Id = 2, Title = "3 beds"},
new RoomType {Id = 3, Title = "4 beds"}
};
Â
foreach (var type in roomTypes)
{
context.RoomTypes.Add(type);
}
Â
var roomStatuses = new List<RoomStatus>
{
new RoomStatus { Id = 0, Title = "Ready" },
new RoomStatus { Id = 1, Title = "Dirty" },
new RoomStatus { Id = 2, Title = "Clean up" }
};
Â
foreach (var status in roomStatuses)
{
context.RoomStatuses.Add(status);
}
Â
var rooms = new List<Room>
{
new Room {Number = 101, RoomStatus = roomStatuses[0], RoomType = roomTypes[0]},
new Room {Number = 102, RoomStatus = roomStatuses[1], RoomType = roomTypes[3]},
new Room {Number = 103, RoomStatus = roomStatuses[2], RoomType = roomTypes[2]},
new Room {Number = 104, RoomStatus = roomStatuses[1], RoomType = roomTypes[3]},
new Room {Number = 105, RoomStatus = roomStatuses[0], RoomType = roomTypes[1]},
new Room {Number = 201, RoomStatus = roomStatuses[2], RoomType = roomTypes[0]},
new Room {Number = 202, RoomStatus = roomStatuses[1], RoomType = roomTypes[0]},
new Room {Number = 203, RoomStatus = roomStatuses[1], RoomType = roomTypes[1]},
new Room {Number = 204, RoomStatus = roomStatuses[2], RoomType = roomTypes[2]},
new Room {Number = 301, RoomStatus = roomStatuses[0], RoomType = roomTypes[1]},
new Room {Number = 302 , RoomStatus = roomStatuses[0], RoomType = roomTypes[0]}
};
Â
foreach (var room in rooms)
{
context.Rooms.Add(room);
}
Â
var bookingStatuses = new List<BookingStatus>()
{
new BookingStatus { Id = 0, Title = "New" },
new BookingStatus { Id = 1, Title = "Confirmed" },
new BookingStatus { Id = 2, Title = "Arrived" },
new BookingStatus { Id = 3, Title = "Checked Out" }
};
Â
foreach (var status in bookingStatuses)
{
context.BookingStatuses.Add(status);
}
Â
var bookings = new List<Booking>
{
new Booking { Name = "William T. Gleason", StartDate = DateTime.Now.AddDays(-4), EndDate = DateTime.Now.AddDays(3), IsPaid = true, BookingStatus = bookingStatuses[1], Room = rooms[4] }
};
Â
foreach (var booking in bookings)
{
context.Bookings.Add(booking);
}
Â
context.SaveChanges();
}
}
Now we should include the new db initializer to RoomBookingContext class:
public class RoomBookingContext : DbContext
{
public RoomBookingContext()
{
Database.SetInitializer(new DbInitializer());
}
Â
public DbSet<Room> Rooms { get; set; }
public DbSet<RoomStatus> RoomStatuses { get; set; }
public DbSet<RoomType> RoomTypes { get; set; }
Â
public DbSet<Booking> Bookings { get; set; }
public DbSet<BookingStatus> BookingStatuses { get; set; }
}
Add the "Repository" class with the implemented repository pattern:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
private RoomBookingContext _context;
Â
public Repository()
{
_context = new RoomBookingContext();
}
Â
public Repository(RoomBookingContext context)
{
_context = context;
}
Â
public void Create(TEntity entity)
{
_context.Set<TEntity>().Add(entity);
_context.SaveChanges();
}
Â
public TEntity Read(int id)
{
return _context.Set<TEntity>().Find(id);
}
Â
public void Update(TEntity entity)
{
_context.Set<TEntity>().Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
_context.SaveChanges();
}
Â
public void Delete(int id)
{
var set = _context.Set<TEntity>();
var entity = set.Find(id);
set.Remove(entity);
_context.SaveChanges();
}
Â
public IEnumerable<TEntity> ReadWhere(Func<TEntity, bool> predicate)
{
return _context.Set<TEntity>().Where(predicate);
}
Â
public IEnumerable<TEntity> ReadAll()
{
return _context.Set<TEntity>().AsEnumerable();
}
}
This is quite a complex task, so if we want to use all scheduler customization responsibilities, we will make some additional scheduler component initialization by JavaScript on the client side.
1) Firstly, let's create .NET MVC5 Empty project called "HotelRoomBooking.Presentation.Mvc5".
2) Set Presentation.MVC5 as a startup project. Then install Scheduler.NET, Entity Framework and JSON.NET packages by NuGet:
PM> Install-Package DHTMLX.Scheduler.NET -ProjectName HotelRoomBooking.Presentation.Mvc5
PM> Install-Package EntityFramework -ProjectName HotelRoomBooking.Presentation.Mvc5
PM> Install-Package Newtonsoft.Json -ProjectName HotelRoomBooking.Presentation.Mvc5
3) Add references to all other solution projects (Infrastructure, Entities and DataAccess).
4) Overload basic routing settings in the AppStart/RouteConfig.cs file:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Calendar", action = "Index", id = UrlParameter.Optional }
);
}
}
5) Let's create a basic MVC structure for our project (consists of folders and files).
*web.config was created automatically.
_ViewStart.cshtml content:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
_Layout.cshtml content
<html>
<head>
<title>@ViewBag.Title</title>
</head>
<body>
@RenderBody()
</body>
</html>
In this controller we will set scheduler initialization settings that will be translated into JavaScript on the client side.
Create an empty CalendarController in the Controllers folder.
In the Index( ... ) action result method we will initialize scheduler via Scheduler.NET and transfer the model into the view to render.
Add a private configured timeline view returning method to the current controller:
private TimelineView ConfigureTimelineView(IEnumerable<object> rooms)
{
var timeline = new TimelineView("Timeline", "room_number");
timeline.RenderMode = TimelineView.RenderModes.Bar;
timeline.X_Unit = TimelineView.XScaleUnits.Day;
timeline.X_Date = "%d";
timeline.X_Size = 45;
timeline.AddSecondScale(TimelineView.XScaleUnits.Month, "%F %Y");
timeline.Dy = 51;
timeline.SectionAutoheight = false;
timeline.RoundPosition = true;
Â
timeline.FullEventDy = true;
Â
timeline.ServerList = "Rooms";
Â
timeline.AddOptions(rooms);
Â
return timeline;
}
The private method that colors weekends in other color in the scheduler:
private void MarkWeekends(DHXScheduler scheduler)
{
string cssClass = "timeline_weekend";
scheduler.TimeSpans.Add(new DHXMarkTime { Day = DayOfWeek.Saturday, CssClass = cssClass});
scheduler.TimeSpans.Add(new DHXMarkTime {Day = DayOfWeek.Sunday, CssClass = cssClass});
}
Add configuring lightbox (new booking addition form) method:
private void ConfigureLightBox(DHXScheduler scheduler, IEnumerable<object> rooms)
{
var name = new LightboxText("text", "Name");
name.Height = 24;
scheduler.Lightbox.Add(name);
Â
var roomsSelect = new LightboxSelect("room_number", "Room");
roomsSelect.AddOptions(rooms);
scheduler.Lightbox.Add(roomsSelect);
Â
var status = new LightboxRadio("status", "Status");
var statuses = new Repository<BookingStatus>().ReadAll().Select(s => new {key = s.Id, label = s.Title});
status.AddOptions(statuses);
scheduler.Lightbox.Add(status);
scheduler.InitialValues.Add("status", statuses.First().key);
Â
var isPaid = new LightboxCheckbox("is_paid", "Paid");
scheduler.Lightbox.Add(isPaid);
scheduler.InitialValues.Add("is_paid", false);
Â
var date = new LightboxMiniCalendar("time", "Time");
scheduler.Lightbox.Add(date);
Â
Â
scheduler.InitialValues.Add("text", String.Empty);
}
Add a general private method that will configure scheduler:
private DHXScheduler ConfigureScheduler()
{
var scheduler = new DHXScheduler();
Â
Â
ViewBag.RoomTypes = JsonConvert.SerializeObject(new Repository<RoomType>().ReadAll());
ViewBag.RoomStatuses = JsonConvert.SerializeObject(new Repository<RoomStatus>().ReadAll());
ViewBag.BookingStatuses = JsonConvert.SerializeObject(new Repository<BookingStatus>().ReadAll());
Â
scheduler.Extensions.Add(SchedulerExtensions.Extension.Limit);
scheduler.Extensions.Add(SchedulerExtensions.Extension.Collision);
scheduler.Config.collision_limit = 1;
Â
var rooms = new Repository<Room>().ReadAll().Select(
r => new
{
key = r.Id,
room_number = r.Number,
label = r.Number,
type = r.RoomTypeId,
status = r.RoomStatusId
}).OrderBy(r => r.room_number).ToList();
Â
scheduler.Skin = DHXScheduler.Skins.Flat;
Â
var timeLine = ConfigureTimelineView(rooms);
scheduler.Views.Clear();
scheduler.Views.Add(timeLine);
scheduler.InitialView = timeLine.Name;
Â
scheduler.BeforeInit.Add("pre_init();");
Â
ConfigureLightBox(scheduler, rooms);
Â
MarkWeekends(scheduler);
Â
return scheduler;
}
Notes:
ServerList - the name of collection of JavaScript for objects that is used in multiple parts of scheduler. It will be generated automatically in our case.
The code below will transfer JSON to the view to share these entities with the client side:
ViewBag.RoomTypes = JsonConvert.SerializeObject(new Repository<RoomType>().ReadAll());
ViewBag.RoomStatuses = JsonConvert.SerializeObject(new Repository<RoomStatus>().ReadAll());
ViewBag.BookingStatuses = JsonConvert.SerializeObject(new Repository<BookingStatus>().ReadAll());
scheduler.Extensions.Add(SchedulerExtensions.Extension.Limit); - adds red today line on the view;
Â
scheduler.Extensions.Add(SchedulerExtensions.Extension.Collision); - checks collision on the client-side
Rooms will be transferred to the server automatically on initial data loading, because it was used in lightbox (Scheduler.NET feature).
Code below will execute JavaScript pre-initialization function that will be described later
scheduler.BeforeInit.Add("pre_init();");
Add the Index ( ... ) method and generate an empty view for it:
public ActionResult Index()
{
return View(ConfigureScheduler());
}
View (Views / Calendar / Index.cshtml)
@model DHTMLX.Scheduler.DHXScheduler
Â
<link rel="stylesheet" href="~/Content/Site.css"/>
<script src="~/Scripts/scheduler_initialization.js"></script>
<script>
var roomTypesCollection = @Html.Raw(ViewBag.RoomTypes);
var roomStatusesCollection = @Html.Raw(ViewBag.RoomStatuses);
var bookingStatusesCollection = @Html.Raw(ViewBag.BookingStatuses);
</script>
<div style="width: 100%; height: 600px; margin: 0 auto;">
@Html.Raw(@Model.Render())
</div>
Gray marked text will initialize JavaScript collections that were described previously.
The scheduler_initialization.js file will be described below.
The created scheduler_initialization.js file will contain additional client-side scheduler initialization. It will have post-, pre-initialization and some help functions.
Firstly, let's create help functions:
// returns title of room types, room statuses and booking statuses
// collections were predefined on view:
// var roomTypesCollection
// var roomStatusesCollection
// var bookingStatusesCollection
function titleById(collection, id) {
for (var i = 0; i < collection.length; i++) {
if (collection[i].Id == id) {
return collection[i].Title;
}
}
return "";
}
function formatDateForEventBox(date) {
var formatFunc = scheduler.date.date_to_str("%d %M %Y");
return formatFunc(date);
}
Now let's write and fill the pre_init() function. It will be executed before scheduler rendering.
Set the room item template (will be described later):
scheduler.templates.Timeline_scale_label = function (key, label, room) {
var indicatorClass = "room_status_indicator_" + room.status;
Â
var template = "<div class='timeline_item_separator'></div>" +
"<div class='timeline_item_cell'>" + room.room_number + "</div>" +
"<div class='timeline_item_separator'></div>" +
"<div class='timeline_item_cell'>" + titleById(roomTypesCollection, room.type) + "</div>" +
"<div class='timeline_item_separator'></div>" +
"<div class='timeline_item_cell room_status'>" + "<span class='room_status_indicator " + indicatorClass + "'></span>" + "<span>" + titleById(roomStatusesCollection, room.status) + "</span>" + "</div>";
Â
return template;
}
Set one-month switch step:
scheduler.date.Timeline_start = scheduler.date.month_start;
scheduler.date.add_Timeline = function (date, step) {
return scheduler.date.add(date, step, "month");
};
scheduler.attachEvent("onBeforeViewChange", function (old_mode, old_date, mode, date) {
var year = date.getFullYear();
var month = (date.getMonth() + 1);
var d = new Date(year, month, 0);
var daysInMonth = d.getDate();
scheduler.matrix["Timeline"].x_size = daysInMonth;
return true;
});
Add events coloring:
scheduler.templates.event_class = function (start, end, event) {
return "event_" + (event.status || "");
}
Paid and status properties displaying for events:
scheduler.templates.event_bar_text = function (start, end, event) {
var paidStatus = event.is_paid == true ? "paid" : "not paid";
var startDate = formatDateForEventBox(event.start_date);
var endDate = formatDateForEventBox(event.end_date);
var dates = startDate + " - " + endDate;
var statusDiv = "<div class='booking_status booking-option'>" +
(titleById(bookingStatusesCollection, event.status) || "") + "</div>";
var paidDiv = "<div class='booking_paid booking-option'>" + paidStatus + "</div>";
Â
var output = event.text + "<br />" + dates + statusDiv + paidDiv;
return output;
};
Client-side collision message display definition:
scheduler.attachEvent("onEventCollision", function (ev, evs) {
for (var i = 0; i < evs.length; i++) {
if (ev.room_number != evs[i].room_number) continue;
dhtmlx.message({
type: "error",
text: "This room is already booked for this date."
});
}
return true;
});
Add rooms description definition:
var element = document.getElementById("scheduler_here");
// first +1 -- blank space upper border, second +1 -- hardcoded border length
var top = scheduler.xy.nav_height + 1 + 1;
var height = scheduler.xy.scale_height * 2;
var width = scheduler.matrix.Timeline.dx;
var descriptionHTML = "<div class='timeline_item_separator'></div>" +
"<div class='timeline_item_cell'>Number</div>" +
"<div class='timeline_item_separator'></div>" +
"<div class='timeline_item_cell'>Type</div>" +
"<div class='timeline_item_separator'></div>" +
"<div class='timeline_item_cell room_status'>Status</div>";
descriptionHTML = "<div style='position: absolute; top:"+ top +"px; width:" + width + "px; height:" + height + "px;'>" +
descriptionHTML + "</div><div style='clear: both;'></div>";
element.innerHTML += descriptionHTML;
Here we will fill the Site.css file in the Content folder.
html, body {
font-family: "Segoe UI";
}
.dhx_cal_tab_standalone {
display: none;
}
.timeline_item_cell {
float: left;
width: 32%;
height: 100% !important;
font-size: 12px;
text-align: center;
line-height: 50px;
}
.room_status {
position: relative;
}
.timeline_item_separator {
float: left;
background-color: #CECECE;
width: 1px;
height: 100% !important;
}
.room_status_indicator {
position: absolute;
background-color: red;
left: 0;
top: 0;
right: 95%;
bottom: 0;
}
.room_status_indicator_1 {
background-color: #4CAF50;
}
.room_status_indicator_2 {
background-color: red;
}
.room_status_indicator_3 {
background-color: #FFA000;
}
.timeline_weekend {
background-color: #FFF9C4;
}
.dhx_cal_event_line {
background-color: #FFB74D !important;
}
Set booking colors that will depend on the status id:
.event_1 {
background-color: #FFB74D !important;
}
.event_2 {
background-color: #9CCC65 !important;
}
.event_3 {
background-color: #40C4FF !important;
}
.event_4 {
background-color: #BDBDBD !important;
}
.booking_status {
position: absolute;
top: 2px;
right: 2px;
}
.booking_paid {
position: absolute;
bottom: 2px;
right: 2px;
}
.dhx_cal_event_line:hover .booking-option {
background: none !important;
}
.dhx_section_time select {display:none}
.dhx_mini_calendar .dhx_year_week,
.dhx_mini_calendar .dhx_scale_bar{
height: 30px !important;
}
.dhx_cal_light_wide .dhx_section_time {
text-align: left;
}
.dhx_cal_light_wide .dhx_section_time > input:first-child {
margin-left: 10px;
}
.dhx_cal_light_wide .dhx_section_time input{
border: 1px solid #aeaeae;
padding-left:5px;
}
Now the scheduler is configured, but it doesn't send or load any data to the server on actions with bookings. So, it's time to fix it.
Let's create an empty controller DataAccessController in the Controllers folder.
Our application will load bookings dynamically, so the method returning bookings data will receive time period dates as parameters from the response. Add a method to the controller:
public ActionResult Bookings()
{
var dateFrom = DateTime.ParseExact(this.Request.QueryString["from"], "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
var dateTo = DateTime.ParseExact(this.Request.QueryString["to"], "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture);
Â
var bookings = new Repository<Booking>().ReadAll().Where(e => e.StartDate <= dateTo && e.EndDate >= dateFrom);
return new SchedulerAjaxData(bookings);
}
Then enable data loading for scheduler: add the code to ConfigureScheduler( ... ) in CalendarController (insert before "return scheduler").
scheduler.EnableDynamicLoading(SchedulerDataLoader.DynamicalLoadingMode.Month);
scheduler.Config.show_loading = true;
scheduler.LoadData = true;
scheduler.DataAction = Url.Action("Bookings", "DataAccess");
The line
scheduler.EnableDynamicLoading(SchedulerDataLoader.DynamicalLoadingMode.Month);
enables loading of bookings of the current month (optimization).
Now our application loads bookings from the database.
Now we will implement data saving on changes on the client side.
The Save( ... ) method in DataAccessController will receive data from the client for update:
public ActionResult Save(int? id, FormCollection actionValues)
{
var action = new DataAction(actionValues);
var repo = new Repository<Booking>();
Â
try
{
var changedBooking = DHXEventsHelper.Bind<Booking>(actionValues);
Â
switch (action.Type)
{
case DataActionTypes.Insert:
repo.Create(changedBooking);
break;
case DataActionTypes.Delete:
repo.Delete(Convert.ToInt32(action.SourceId));
break;
case DataActionTypes.Update:
repo.Update(changedBooking);
break;
}
action.TargetId = changedBooking.Id;
}
catch (Exception e)
{
action.Type = DataActionTypes.Error;
}
Â
return new AjaxSaveResponse(action);
}
The code string
var changedBooking = DHXEventsHelper.Bind<Booking>(actionValues);
in this method composes entity from POST parameters.
Add strings below (insert to ConfigureScheduler( ... ) method of CalendarController before "return scheduler") to enable data saving to the scheduler:
scheduler.EnableDataprocessor = true;
scheduler.SaveAction = Url.Action("Save", "DataAccess");
Now the scheduler can save and load data from database.
If several users work with our application simultaneously, they may assign different bookings to the same time, because we have only the client-side collision check.
So, we can check collision on server and alarm the current user, if somebody else has assigned another booking to the selected time. All this will work on page refresh.
Create a collision check helper method in DataAccessController:
private bool IsCollidesWithOthers(Booking booking, IRepository<Booking> repo)
{
var collidedBookings =
new Repository<Booking>().ReadWhere(
b => b.Id != booking.Id && b.RoomId == booking.RoomId && b.StartDate < booking.EndDate && b.EndDate > booking.StartDate);
Â
if (collidedBookings.Any()) return true;
Â
return false;
}
Add a collision check into the Save ( ... ) method
(after var changedBooking = DHXEventsHelper.Bind
if (action.Type != DataActionTypes.Delete && IsCollidesWithOthers(changedBooking, repo))
{
action.Type = DataActionTypes.Error;
action.Message = "This room is already booked for this date.";
return new AjaxSaveResponse(action);
}
The action type check is necessary for a situation when a user can find an unforeseen method of assigning 2 bookings to the same time. It will give the ability to avoid collision check to delete some booking.
Now we should add an error displaying and data reloading on the client side.
Firstly, insert the post_init( ) function into the scheduler_initialization.js file:
function post_init() {
scheduler.dataProcessor.attachEvent("onAfterUpdate", function (id, action, tid, response) {
if (action == "error") {
dhtmlx.message({
type: "error",
text: response.textContent || response.nodeValue
});
scheduler.clearAll();
scheduler.load(dataActionUrl, "json"); // dataActuionUrl receiving from ViewBag.DataAction in the beginning of the view
}
});
}
dataActionUrl will be initialized in the top of the view and shared from the server by ViewBag. Add the code below to ConfigureScheduler ( ... ) of CalendarController (before "return scheduler"):
ViewBag.DataAction = scheduler.DataAction;
Next, initialize dataAction URL in Index.cshtml:
@model DHTMLX.Scheduler.DHXScheduler
Â
<link rel="stylesheet" href="~/Content/Site.css" />
<script src="~/Scripts/scheduler_initialization.js"></script>
Â
<script>
var dataActionUrl = "@ViewBag.DataAction";
Â
var roomTypesCollection = @Html.Raw(ViewBag.RoomTypes);
var roomStatusesCollection = @Html.Raw(ViewBag.RoomStatuses);
var bookingStatusesCollection = @Html.Raw(ViewBag.BookingStatuses);
</script>
Â
<div style="width: 100%; height: 600px; margin: 0 auto;">
@Html.Raw(@Model.Render())
</div>
Set post_init( ) as post initialization function to scheduler: insert the string below to ConfigureScheduler ( ... ) of CalendarController (before "return scheduler").
scheduler.AfterInit.Add("post_init();");
Done. Hotel room booking calendar is ready to use.
You can download the ready sample of a hotel room booking calendar.