we divided the whole tutorial into three parts:
a) Initialization
As the first step, we create a new ASP.NET MVC 3 Web Application with the Razor view engine.
For initialization add the .dll library reference and scripts from the DHTMLX Scheduler .NET package to your project folder. If you need help with that, please, refer to the items 2-3 of our ASP.NET simple event calendar tutorial.
b) Database and Model Creation
Right-click on your project name in the Solution Explorer and add a new ASP.NET folder App_Data. Now create a new SQL Server Database and name it MyScheduler.mdf
To set up the application database create 3 tables – Type,Car and Order – and add the required columns:
The ‘Type’ table should only have the car id and the title values:
Go to
Views, create a new folder ‘Home’ and add Index.cshtml. Delete the
views we do not require from the folders Home and Shared, that’s
Error.cshtml and About.cshtml.
Output data generated with the car search form are stored in the FormState:
b) Controller and View
Date is selected from the two inputs – date picker and ‘select time’ drop-down list. We add a function to HomeController.cs to parse the date to the DataTime object.
- Implementation of the Main Functionality;
- Creation of Date and Time Filter;
- Signing Rent Boxes with Rent Duration Period;
After completing the above mentioned steps, you’ll get a ready car rental service like on the picture below:
You can skip reading this tutorial and simply download a ready package right now.
Part I. Implementation of the Main Functionality
Let’s create a simple application with a calendar that has a vertical car arrangement and a search field to filter the available cars by price and type.a) Initialization
As the first step, we create a new ASP.NET MVC 3 Web Application with the Razor view engine.
For initialization add the .dll library reference and scripts from the DHTMLX Scheduler .NET package to your project folder. If you need help with that, please, refer to the items 2-3 of our ASP.NET simple event calendar tutorial.
b) Database and Model Creation
Right-click on your project name in the Solution Explorer and add a new ASP.NET folder App_Data. Now create a new SQL Server Database and name it MyScheduler.mdf
To set up the application database create 3 tables – Type,Car and Order – and add the required columns:
The ‘Type’ table should only have the car id and the title values:
The ‘Car’ table should include data to identify cars price, brand, type and image:
Let’s make the TypeId field a foreign key that refers to [Type].id field.
The ‘Order’ table is used to enable order placement with a car rental form. Therefore it should contain description, time period, pick up/drop off locations and car id columns:
The ‘Order’ table is used to enable order placement with a car rental form. Therefore it should contain description, time period, pick up/drop off locations and car id columns:
Like with the ‘Car’, table make’ the Car_id field a foreign key that refers to [Car].id field.
Set primary key to the id columns of the three tables. Remember to change the properties of the identity column to id.
When the tables are completed, create a data model LINQ to SQL Classes. Name it Rental and drag the newly created tables onto the LINQ to SQL designer surface.
When the tables are completed, create a data model LINQ to SQL Classes. Name it Rental and drag the newly created tables onto the LINQ to SQL designer surface.
To
facilitate rendering of the Order collection as a json string during
data loading, no cyclic links between ‘Order’ and ‘Type’ should be set.
To achieve this, change Parent Property Access from Public to Internal for each association:
To achieve this, change Parent Property Access from Public to Internal for each association:
Right-click on the ‘Models’ to create a ViewModel.cs with the Scheduler object and a category (car) count:
1
2
3
4
5
6
7
8
9
10
11
12
13
| using System; using System.Collections.Generic; using System.Linq; using System.Web; using DHTMLX.Scheduler; namespace Rental.Models { public class ViewModel { public DHXScheduler Scheduler { get ; set ; } public int CategoryCount { get ; set ; } } |
1
2
3
4
5
| public class FormState { public string Type { get ; set ; } public string Price { get ; set ; } } |
To change our website layout go to Views -> Shared -> _Layout.cshtml :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| <!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> <link href= "@Url.Content(" ~/Content/Site.css ")" rel= "stylesheet" type= "text/css" /> <script src= "@Url.Content(" ~/Scripts/jquery-1.7.2.min.js ")" type= "text/javascript" ></script> </head> <body> <div class = "page" > <div id= "header" > </div> <div id= "main" > @RenderBody() <div id= "footer" > </div> </div> </div> </body> </html> |
Go to Home -> Index.cshtml to make changes also to the calendar page.
The full code at this stage should look as follows:
The full code at this stage should look as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
| @{ ViewBag.Title = "Car Rental Service" ; } @model Rental.Models.ViewModel @Html.Raw(Model.Scheduler.GenerateCSS()) @Html.Raw(Model.Scheduler.GenerateJS()) <style type= "text/css" > /*override some scheduler css*/ #scheduler_here .dhx_cal_tab { display:none; } .dhx_cal_header div div { border:none; } .dhx_cal_data { background-color:#fff; } .dhx_cal_event_line { background-image:none; background-color:#FFDD71; font-size:13px; line-height:@((Model.Scheduler.Views[0] as DHTMLX.Scheduler.Controls.TimelineView).EventDy)px; } .dhx_matrix_scell { max-width:@((Model.Scheduler.Views[0] as DHTMLX.Scheduler.Controls.TimelineView).Dx)px; } .dhx_cal_event_line .dhx_event_resize { background-image: none; } </style> <script> //define template for timeline units function def_template(){ scheduler.templates[ '@((Model.Scheduler.Views[0]).Name)_scale_label' ]= function(key, label, obj){ return "<div style=\"width:100%\">\ <img src=\ "" +obj.link+ "\" alt=\"" +label+"\"></img><br/>\ <div class =\ "car_brand\">" +label+ "</div><div class=\"car_price\">$" +obj.price+ "</div></div>" ; }; scheduler.templates.event_bar_text = function (start, end, event ) { return "Rented" ; }; } </script> <div style= "height:750px" > <div class = "message" > @ViewBag.Message </div> <div style= "float:left; width:230px;height:100%;" > @ using (Html.BeginForm( "Index" , "Home" , FormMethod.Post)) { <div class = "search_form" > <div class = "form_head" > <span class = "rent_title" >Rent a Car</span><br /> <span class = "rent_small" >with DHTMLX Scheduler .NET</span><br /> <div class = "hd_line" ></div> </div> <div class = "controls" > <div> @Html.Label( "Type" , "Type:" )<br /> @Html.DropDownList( "Type" ) </div> <div> @Html.Label( "Price" , "Price:" )<br /> @Html.DropDownList( "Price" ) </div> <div> <input type= "submit" id= "submit" value= "Search" /> </div> </div> </div> } </div> <div style= "float:left; width:950px;height:100%;" > @ if (! string .IsNullOrEmpty(( string )ViewData[ "Message" ] )) { <script> dhtmlx.message( "@ViewData[" Message "]" ); </script> } @{ //calculate height of calendar container int rowHeigth = (Model.Scheduler.Views[0] as DHTMLX.Scheduler.Controls.TimelineView).Dy; int headerHeight = 45; int showRows = 7; int actualHeight = rowHeigth * Model.CategoryCount + headerHeight; int maxHeight = showRows * rowHeigth + headerHeight; } <div style= "height:@(actualHeight < maxHeight ? actualHeight : maxHeight)px;" > @Html.Raw(Model.Scheduler.GenerateHTML()) </div> </div> <div class = "clear" ></div> <script> //after created/edited rent order - navigate calendar to its start date scheduler.attachEvent( "onEventSave" , function (id, data, is_new_event) { if (data.start_date) { scheduler.setCurrentView( new Date(data.start_date)); } return true ; }); </script> </div> |
Below
you can find detailed descriptions of how we implement Scheduler .NET
rendering and initialization as well as customize the calendar template :
1. Scheduler .NET rendering
This time we render the Scheduler in an unusual way to override some default css styles after loading and before Scheduler initialization. Instead of using Scheduler.Render () we did in the following way:
At first, we loaded the required css and js:
This time we render the Scheduler in an unusual way to override some default css styles after loading and before Scheduler initialization. Instead of using Scheduler.Render () we did in the following way:
At first, we loaded the required css and js:
1
2
3
| @Html.Raw(Model.Scheduler.GenerateCSS()) @Html.Raw(Model.Scheduler.GenerateJS()) |
And finalized it with generating the markup and initialization code:
1
| @Html.Raw(Model.Scheduler.GenerateHTML()) |
2. Scheduler .NET initialization
The Scheduler initializes in the container of the set height. The number of cars (and consequently, the actual calendar height) can be different. It depends on the search form output. We’ve chosen the default height of 7 lines. If there are < 7 lines, the calendar height is equal to the number of lines. If there are > 7 lines in the calendar, scrolling is enabled while the calendar height is set to the default 7 lines.
@{The Scheduler initializes in the container of the set height. The number of cars (and consequently, the actual calendar height) can be different. It depends on the search form output. We’ve chosen the default height of 7 lines. If there are < 7 lines, the calendar height is equal to the number of lines. If there are > 7 lines in the calendar, scrolling is enabled while the calendar height is set to the default 7 lines.
1
2
3
4
5
6
7
8
| //calculate height of calendar container int rowHeigth = (Model.Scheduler.Views[0] as DHTMLX.Scheduler.Controls.TimelineView).Dy; int headerHeight = 45; int showRows = 7; int actualHeight = rowHeigth * Model.CategoryCount + headerHeight; int maxHeight = headerHeight + showRows * rowHeigth; } <div style= "height:@(actualHeight < maxHeight ? actualHeight : maxHeight)px;" > |
3. Scheduler .NET template customization
We override templates for the car scale and rent boxes:
1
2
3
4
5
6
7
8
9
10
| scheduler.templates[ '@((Model.Scheduler.Views[0]).Name)_scale_label' ]= function(key, label, obj){ return "<div style=\"width:100%\">\ <img src=\ "" +obj.link+ "\" alt=\"" +label+"\"></img><br/>\ <div class =\ "car_brand\">" +label+ "</div><div class=\"car_price\">$" +obj.price+ "</div></div>" ; }; scheduler.templates.event_bar_text = function (start, end, event ) { return "Rented" ; }; |
Proceed with adding the required classes to Site.css:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
| body { background: #5c87b2; background-image: url( "./background_car_rent.png" ); font-size: 75%; font-family:Arial,sans-serif; margin: 0; padding: 0; color: #696969; } .hd_line { height:1px; background-image: url( "./line.png" ); width:100%; margin:10px 0 20px; } .rent_title { color: #d4f4fc; font-size:18px; } .rent_small { color: #d4e6fc; font-size:10px; } .search_form { width:173px; height:350px; padding:15px 20px; background-repeat:repeat-x; background-image: url( "./bg_form.png" ); } .message { text-align:right; color:White; font-size:18px; height:24px; padding:5px; } .car_brand, .car_price { border-left:none !important; overflow:hidden; } .car_price { color:Gray; width:100%; font-size:10px; margin-top:2px; text-align:center; } .car_brand { white-space:nowrap; margin-top:-4px; color:Black; font-weight:bold; } .search_form input { height:18px; } .search_form select { height:19px; vertical-align:top; } .search_form option { vertical-align:middle; } .search_form select, .search_form input { font-size:11px; width:170px; padding: 0; margin: 5px 0; } #submit { width: 170px; height: 23px; border: 0 none; background-color: #EFEFEF; line-height: 23px; margin-top: 15px; font-weight:bold; } .search_form { border:none; color:#eee; } .search_form .controls > div { margin-top:9px; } /* PRIMARY LAYOUT ELEMENTS ----------------------------------------------------------*/ /* you can specify a greater or lesser percentage for the page width. Or, you can specify an exact pixel width. */ .page { width: 90%; margin-left: auto; margin-right: auto; } #main { padding: 30px 30px 15px 30px; _height: 1px; /* only IE6 applies CSS properties starting with an underscore */ } .clear { clear: both; } .page { width:1200px; } #main { padding:0; } .minical_container { position:absolute; width:200px; } |
c) Create a controller
The next stage is to create a controller. Right-click on the Controllers folder in the Solution Explorer and create HomeController.cs.
The full code should look like this:
The next stage is to create a controller. Right-click on the Controllers folder in the Solution Explorer and create HomeController.cs.
The full code should look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
| using System.Linq; using System.Web; using System.Web.Mvc; using System.Collections; using System.Collections.Generic; using DHTMLX.Scheduler; using DHTMLX.Scheduler.Controls; using DHTMLX.Scheduler.Data; using DHTMLX.Common; using Rental.Models; namespace Rental.Controllers { public class HomeController : Controller { public ActionResult Index(FormState state) { var scheduler = new DHXScheduler( this ); scheduler.Extensions.Add(SchedulerExtensions.Extension.Collision); scheduler.Extensions.Add(SchedulerExtensions.Extension.Minical); //call custom template initialization scheduler.BeforeInit.Add( "def_template();" ); scheduler.Config.time_step = 60; //set row height scheduler.XY.bar_height = 76; scheduler.InitialValues.Add( "text" , "" ); var context = new RentalDataContext(); //selecting cars according to form values var cars = _SelectCars(context, state); //if no cars found - show message and load default set if (cars.Count() == 0) { ViewData[ "Message" ] = "Nothing was found on your request" ; cars = _SelectCars(context); //select default set of events } //create custom details form var printableList = cars.Select(c => new { key = c.id, label = c.Brand, price = c.Price, link = c.Photo }); _ConfigureLightbox(scheduler, printableList); //load cars to the timeline view _ConfigureViews(scheduler, printableList); //assign ViewData values _UpdateViewData(scheduler, context, state); //data loading/saving settings scheduler.PreventCache(); scheduler.LoadData = true ; scheduler.EnableDataprocessor = true ; //collect model var model = new ViewModel(); model.Scheduler = scheduler; model.CategoryCount = cars.Count(); return View(model); } protected void _UpdateViewData(DHXScheduler scheduler, RentalDataContext context, FormState state) { ViewData[ "Price" ] = _CreatePriceSelect(scheduler, state.Price); ViewData[ "Type" ] = _CreateTypeSelect(context.Types, state.Type); } private List<SelectListItem> _CreateTypeSelect(IEnumerable<Models.Type> types, string selected) { var typesList = new List<SelectListItem>() { new SelectListItem(){Value = "" , Text = "Any" } }; foreach (var type in types) { var item = new SelectListItem() { Value = type.id.ToString(), Text = string .Format( "{0}: {1} cars" , type.title, type.Cars.Count) }; if (item.Value == selected) item.Selected = true ; typesList.Add(item); } return typesList; } private List<SelectListItem> _CreatePriceSelect(DHXScheduler scheduler, string selected) { var priceRanges = new string [] { "50-80" , "80-120" , "120-150" }; var prices = new List<SelectListItem>(){ new SelectListItem(){Value = "" , Text = "Any" } }; foreach (var pr in priceRanges) { var item = new SelectListItem() { Value = pr, Text = string .Format( "${0}" , pr) }; if (pr == selected) item.Selected = true ; prices.Add(item); } return prices; } protected IQueryable<Car> _SelectCars(RentalDataContext context) { return _SelectCars(context, null ); } protected IQueryable<Car> _SelectCars(RentalDataContext context, FormState state) { IQueryable<Car> cars = from car in context.Cars select car; if (state == null ) return cars; string _type = state.Type; string _price = state.Price; //filter by car type if (! string .IsNullOrEmpty(_type)) { int type = context.Types.First().id; if (! string .IsNullOrEmpty(_type)) { int .TryParse(_type, out type); } cars = cars.Where(c => c.TypeId == type); } //filter by price if (! string .IsNullOrEmpty(_price)) { var price = _price.Split( '-' ); int low = int .Parse(price[0]); int top = int .Parse(price[1]); cars = cars.Where(c => c.Price <= top && c.Price >= low); } return cars; } protected void _ConfigureViews(DHXScheduler scheduler, IEnumerable cars) { var units = new TimelineView( "Orders" , "car_id" ); units.X_Step = 2; units.X_Length = 12; units.X_Size = 12; //width of the first column units.Dx = 149; //row height units.Dy = 76; //rent boxes height units.EventDy = units.Dy - 5; units.AddOptions(cars); units.RenderMode = TimelineView.RenderModes.Bar; scheduler.Views.Clear(); scheduler.Views.Add(units); scheduler.InitialView = scheduler.Views[0].Name; } protected void _ConfigureLightbox(DHXScheduler scheduler, IEnumerable cars) { scheduler.Lightbox.Add( new LightboxText( "text" , "Contact details" ) { Height = 42, Focus = true }); scheduler.Lightbox.Add( new LightboxText( "description" , "Note" ) { Height = 63 }); var select = new LightboxSelect( "car_id" , "Car Brand" ); scheduler.Lightbox.Add(select); scheduler.Lightbox.Add( new LightboxText( "pick_location" , "Pick up location" ) { Height = 21 }); scheduler.Lightbox.Add( new LightboxText( "drop_location" , "Drop off location" ) { Height = 21 }); select.AddOptions(cars); scheduler.Lightbox.Add( new LightboxTime( "time" , "Time period" )); } public ContentResult Data() { return new SchedulerAjaxData(( new RentalDataContext()).Orders); } public ContentResult Save( int ? id, FormCollection actionValues) { var action = new DataAction(actionValues); RentalDataContext data = new RentalDataContext(); try { var changedEvent = (Order)DHXEventsHelper.Bind( typeof (Order), actionValues); switch (action.Type) { case DataActionTypes.Insert: data.Orders.InsertOnSubmit(changedEvent); break ; case DataActionTypes.Delete: changedEvent = data.Orders.SingleOrDefault(ev => ev.id == action.SourceId); data.Orders.DeleteOnSubmit(changedEvent); break ; default : // "update" var eventToUpdate = data.Orders.SingleOrDefault(ev => ev.id == action.SourceId); DHXEventsHelper.Update(eventToUpdate, changedEvent, new List< string >() { "id" }); break ; } data.SubmitChanges(); action.TargetId = changedEvent.id; } catch { action.Type = DataActionTypes.Error; } return ( new AjaxSaveResponse(action)); } } } |
Let’s have an insight into the most essential code snippets:
First of all, add ‘Collision extension’ to avoid multiple orders of one and the same car for the same period. At this step we also connect a mini-calendar that will be later required:
First of all, add ‘Collision extension’ to avoid multiple orders of one and the same car for the same period. At this step we also connect a mini-calendar that will be later required:
1
2
3
4
5
6
| public ActionResult Index(FormState state) { var scheduler = new DHXScheduler( this ); scheduler.Extensions.Add(SchedulerExtensions.Extension.Collision); scheduler.Extensions.Add(SchedulerExtensions.Extension.Minical); |
It’s easy to implement filtration with LINQ to SQL. We simply add several where filters.
1
2
3
4
5
6
7
8
| protected IQueryable<Car> _SelectCars(RentalDataContext context, FormState state) { IQueryable<Car> cars = from car in context.Cars select car; if (state == null ) return cars; string _type = state.Type; string _price = state.Price; |
It means, LINQ doesn’t filter the collection each time when we call Where(). LINQ
to SQL will translate these conditions into the equivalent SQL query
and send it to the SQL server for processing just before cars collection is enumerated.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| //filter by car type if (! string .IsNullOrEmpty(_type)) { int type = context.Types.First().id; if (! string .IsNullOrEmpty(_type)) { int .TryParse(_type, out type); } cars = cars.Where(c => c.TypeId == type); } //filter by price if (! string .IsNullOrEmpty(_price)) { var price = _price.Split( '-' ); int low = int .Parse(price[0]); int top = int .Parse(price[1]); cars = cars.Where(c => c.Price <= top && c.Price >= low); } return cars; } |
We
set a 2 step time interval for car order with the scale of 12 cells in
the TimelineView. Columns and rent boxes height as well as the height of
rows are also set here. The function scheduler.Views.Clear(); removes the default scheduler views.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| var units = new TimelineView( "Orders" , "car_id" ); units.X_Step = 2; units.X_Length = 12; units.X_Size = 12; //width of the first column units.Dx = 149; //row height units.Dy = 76; //rent boxes height units.EventDy = units.Dy - 5; units.AddOptions(cars); units.RenderMode = TimelineView.RenderModes.Bar; scheduler.Views.Clear(); scheduler.Views.Add(units); scheduler.InitialView = scheduler.Views[0].Name; |
Create a list of select options in the controller and pass them to view via ViewData. Retain the selected values in the controls after page reload.
1
2
3
4
5
| protected void _UpdateViewData(DHXScheduler scheduler, RentalDataContext context, FormState state) { ViewData[ "Price" ] = _CreatePriceSelect(scheduler , state.Price); ViewData[ "Type" ] = _CreateTypeSelect(context.Types, state.Type); } |
Let’s customize the customer’s form for order placement by adding the following code:
1
2
3
4
5
6
7
8
9
10
11
12
| protected void _ConfigureLightbox(DHXScheduler scheduler, IEnumerable cars) { scheduler.Lightbox.Add( new LightboxText( "text" , "Contact details" ) { Height = 42, Focus = true }); scheduler.Lightbox.Add( new LightboxText( "description" , "Note" ) { Height = 63 }); var select = new LightboxSelect( "car_id" , "Car Brand" ); scheduler.Lightbox.Add(select); scheduler.Lightbox.Add( new LightboxText( "pick_location" , "Pick up location" ) { Height = 21 }); scheduler.Lightbox.Add( new LightboxText( "drop_location" , "Drop off location" ) { Height = 21 }); select.AddOptions(cars); scheduler.Lightbox.Add( new LightboxTime( "time" , "Time period" )); } |
The form will look like the picture below:
Note: Save and Data methods we use in this app are described in the Simple ASP.NET MVC application with Scheduler.
Copy the
available car images (image size is 80x48 px) to the Content folder of
your project. Right-click on the database in the Server Explorer to
create a new query. Add the car rental static data to the tables Cars
and Types, and execute the SQL file. The data and the necessary pics are
available in the download package.
The first part
of the tutorial is completed. Now you have a nice-looking basic rental
calendar with vertically arranged carsand a simple price and time
select:
Part II. Creation of Date and Time Filter
In the second part of the tutorial we create pick up/drop off date and time selects:
Open Index.cshtml and update it by adding the following time selects:
1
2
3
4
5
6
7
8
9
10
11
12
| <div> <span>Pick Up Date:</span><br /> @Html.TextBox( "DateFrom" ) <img src= "@Url.Content(" ~/Content/calendar.gif ")" class = "date_calendar" onclick= "show_minical('DateFrom');" /> @Html.DropDownList( "TimeFrom" ) </div> <div> <span>Drop Off Date:</span><br /> @Html.TextBox( "DateTo" ) <img src= "@Url.Content(" ~/Content/calendar.gif ")" class = "date_calendar" onclick= "show_minical('DateTo');" /> @Html.DropDownList( "TimeTo" ) </div> |
We also add a checkbox to enable/disable date filters:
1
2
3
4
| <div class = "check_dates" > <span>Only available: </span> @Html.CheckBox( "DateFilter" , true ) </div> |
Go back to Site.css and add styles for the new controls:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| .check_dates { line-height:14px; margin-bottom:-10px; margin-top:0 !important; } .check_dates > input { width:20px; vertical-align:middle; } #DateFrom, #DateTo { width:80px; } #TimeFrom, #TimeTo { width:60px; } .date_calendar { cursor:pointer; height:18px; left:-2px; position:relative; vertical-align:baseline; top:1px; width:18px; } |
Let’s
define the date picker behavior. Go to Scripts folder and create
scripts.js. Add a minical function for the automatic show/ remove of the
date picker:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| /* Date picker behavior */ scheduler.pickerDateFormat = "%m/%d/%Y" ; function remove_minical() { if (scheduler._calendar) { scheduler.destroyCalendar(scheduler._calendar); $( "#minical_cont" ).remove(); scheduler._calendar = null ; } } function show_minical(id) { //if mini calendar already shown - destroy it if (scheduler._calendar) { remove_minical(); } //create container for the mini calendar var position = $( "#" + id).position(); var div = $( "<div></div>" ) .attr( 'id' , 'minical_cont' ).attr( 'class' , 'minical_container' ) .css( 'left' , position.left).css( 'top' , position.top); $( 'body' ).append(div); //create minicalendar scheduler._calendar = scheduler.renderCalendar({ container: "minical_cont" , date: scheduler._date, navigation: true , handler: function (date, calendar) { //on click - put selected value to the input $( "#" + id).val(scheduler.date.date_to_str(scheduler.pickerDateFormat)(date)); //and remove calendar remove_minical(); //check if 'To' date is later than 'From' date if (!areDatesCorrect()) { showWarning(); } } }); function areDatesCorrect() { var from = $( "#DateFrom" ).val(), to = $( "#DateTo" ).val(); if (from && to) { //function to convert string to date object var converter = scheduler.date.str_to_date(scheduler.pickerDateFormat); from = converter(from); to = converter(to); if (from && to) { //if converted successfully if (from.getTime() > to.getTime()) //return false only if start date is later than end date, other cases are valid return false ; } } return true ; } function showWarning() { $( "#DateTo" ).val( "" ); dhtmlx.message( "Pick up date must be before Drop off date!" ); } } |
Update Index.cshtml by connecting the JavaScript library as it is shown below:
1
| <script src= "@Url.Content(" ~/Scripts/scripts.js ")" type= "text/javascript" ></script> |
Now we can adjust the server-side application logic:
a) Model
Update ViewModel.cs by adding properties for the new controls to the FormState:
a) Model
Update ViewModel.cs by adding properties for the new controls to the FormState:
1
2
3
4
5
6
7
8
9
10
| public class FormState { public string DateFrom { get ; set ; } public string DateTo { get ; set ; } public string TimeFrom { get ; set ; } public string TimeTo { get ; set ; } public string Type { get ; set ; } public string Price { get ; set ; } public bool DateFilter { get ; set ; } } |
Date is selected from the two inputs – date picker and ‘select time’ drop-down list. We add a function to HomeController.cs to parse the date to the DataTime object.
1
2
3
4
5
6
7
| private DateTime _ParseDate( string date, string time) { var datestr = string .Format( "{0} {1}" , date, time); DateTime result = new DateTime(); DateTime.TryParse(datestr, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out result); return result; } |
If Pick Up Date is selected, we make it the calendar initial date:
1
2
3
4
5
6
7
8
9
| public ActionResult Index(FormState state) { … if (_ParseDate(state.DateFrom, state.TimeFrom) != default (DateTime)) { scheduler.InitialDate = _ParseDate(state.DateFrom, state.TimeFrom); } … } |
Let’s add one more filter to the method _SelectCars:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| protected IQueryable<Car> _SelectCars(RentalDataContext context) { … var _from = default (DateTime); var _to = default (DateTime); //try to parse time range if (! string .IsNullOrEmpty(state.DateFrom)) { _from = _ParseDate(state.DateFrom, state.TimeFrom); _to = _ParseDate(state.DateTo, state.TimeTo); if (_from.CompareTo( default (DateTime)) != 0 && _to.CompareTo( default (DateTime)) == 0) //only start date set { _to = _from.AddHours(1); } } if (state.DateFilter && _from != default (DateTime) && _to != default (DateTime)) { //select cars, which are available in specified time range cars = from car in cars where car.Orders.Count == 0 || car.Orders.Where(o => o.end_date > _from && o.start_date < _to).Count() == 0 select car ; } return cars; } |
Render the values of new controllers to ViewData:
1
2
3
4
5
6
7
8
9
10
| protected void _UpdateViewData(DHXScheduler scheduler, RentalDataContext context, FormState state) { ViewData[ "Price" ] = _CreatePriceSelect(scheduler , state.Price); ViewData[ "Type" ] = _CreateTypeSelect(context.Types, state.Type) ; ViewData[ "DateFrom" ] = state.DateFrom; ViewData[ "DateTo" ] = state.DateTo; ViewData[ "TimeFrom" ] = _CreateTimeSelect(scheduler, state.TimeFrom); ViewData[ "TimeTo" ] = _CreateTimeSelect(scheduler, state.TimeTo); ViewData[ "DateFilter" ] = state.DateFilter; } |
Here is the full code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
| using System; using System.Collections.Generic; using System.Collections; using System.Linq; using System.Web; using System.Web.Mvc; using DHTMLX.Scheduler; using DHTMLX.Scheduler.Controls; using DHTMLX.Scheduler.Data; using DHTMLX.Common; using Rental.Models; namespace Rental.Controllers { public class HomeController : Controller { public ActionResult Index(FormState state) { var scheduler = new DHXScheduler( this ); scheduler.Extensions.Add(SchedulerExtensions.Extension.Collision); scheduler.Extensions.Add(SchedulerExtensions.Extension.Minical); //call custom template initialization scheduler.BeforeInit.Add( "def_template();" ); scheduler.Config.time_step = 60; //set row height scheduler.XY.bar_height = 76; scheduler.InitialValues.Add( "text" , "" ); //if 'Pick Up Date' selected - make it initial date for calendar if (_ParseDate(state.DateFrom, state.TimeFrom) != default (DateTime)) { scheduler.InitialDate = _ParseDate(state.DateFrom, state.TimeFrom); } var context = new RentalDataContext(); //selecting cars according to form values var cars = _SelectCars(context, state); //if no cars found - show message and load default set if (cars.Count() == 0) { ViewData[ "Message" ] = "Nothing was found on your request" ; cars = _SelectCars(context); //select default set of events } //create custom details form var printableList = cars.Select(c => new { key = c.id, label = c.Brand, price = c.Price, link = c.Photo }); _ConfigureLightbox(scheduler, printableList); //load cars to the timeline view _ConfigureViews(scheduler, printableList); //assign ViewData values _UpdateViewData(scheduler, context, state); //data loading/saving settings scheduler.PreventCache(); scheduler.LoadData = true ; scheduler.EnableDataprocessor = true ; //collect model var model = new ViewModel(); model.Scheduler = scheduler; model.CategoryCount = cars.Count(); return View(model); } protected void _UpdateViewData(DHXScheduler scheduler, RentalDataContext context, FormState state) { ViewData[ "Price" ] = _CreatePriceSelect(scheduler, state.Price); ViewData[ "Type" ] = _CreateTypeSelect(context.Types, state.Type); ViewData[ "DateFrom" ] = state.DateFrom; ViewData[ "DateTo" ] = state.DateTo; ViewData[ "TimeFrom" ] = _CreateTimeSelect(scheduler, state.TimeFrom); ViewData[ "TimeTo" ] = _CreateTimeSelect(scheduler, state.TimeTo); ViewData[ "DateFilter" ] = state.DateFilter; } private List<SelectListItem> _CreateTypeSelect(IEnumerable<Models.Type> types, string selected) { var typesList = new List<SelectListItem>() { new SelectListItem(){Value = "" , Text = "Any" } }; foreach (var type in types) { var item = new SelectListItem() { Value = type.id.ToString(), Text = string .Format( "{0}: {1} cars" , type.title, type.Cars.Count) }; if (item.Value == selected) item.Selected = true ; typesList.Add(item); } return typesList; } private List<SelectListItem> _CreatePriceSelect(DHXScheduler scheduler, string selected) { var priceRanges = new string [] { "50-80" , "80-120" , "120-150" }; var prices = new List<SelectListItem>(){ new SelectListItem(){Value = "" , Text = "Any" } }; foreach (var pr in priceRanges) { var item = new SelectListItem() { Value = pr, Text = string .Format( "${0}" , pr) }; if (pr == selected) item.Selected = true ; prices.Add(item); } return prices; } private List<SelectListItem> _CreateTimeSelect(DHXScheduler scheduler, string selected) { var opts = new List<SelectListItem>(); for (var i = scheduler.Config.first_hour; i < scheduler.Config.last_hour; i++) { var value = string .Format( "{0}:00" , i < 10 ? "0" + i.ToString() : i.ToString()); var item = new SelectListItem() { Text = value, Value = value }; if (value == selected) item.Selected = true ; opts.Add(item); } return opts; } protected IQueryable<Car> _SelectCars(RentalDataContext context) { return _SelectCars(context, null ); } private DateTime _ParseDate( string date, string time) { var datestr = string .Format( "{0} {1}" , date, time); DateTime result = new DateTime(); DateTime.TryParse(datestr, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out result); return result; } protected IQueryable<Car> _SelectCars(RentalDataContext context, FormState state) { IQueryable<Car> cars = from car in context.Cars select car; if (state == null ) return cars; string _type = state.Type; string _price = state.Price; var _from = default (DateTime); var _to = default (DateTime); //try to parse time range if (! string .IsNullOrEmpty(state.DateFrom)) { _from = _ParseDate(state.DateFrom, state.TimeFrom); _to = _ParseDate(state.DateTo, state.TimeTo); if (_from.CompareTo( default (DateTime)) != 0 && _to.CompareTo( default (DateTime)) == 0) //only start date set { _to = _from.AddHours(1); } } //filter by car type if (! string .IsNullOrEmpty(_type)) { int type = context.Types.First().id; if (! string .IsNullOrEmpty(_type)) { int .TryParse(_type, out type); } cars = cars.Where(c => c.TypeId == type); } //filter by price if (! string .IsNullOrEmpty(_price)) { var price = _price.Split( '-' ); int low = int .Parse(price[0]); int top = int .Parse(price[1]); cars = cars.Where(c => c.Price <= top && c.Price >= low); } if (state.DateFilter && _from != default (DateTime) && _to != default (DateTime)) { //select cars, which are available in specified time range cars = from car in cars where car.Orders.Count == 0 || car.Orders.Where(o => o.end_date > _from && o.start_date < _to).Count() == 0 select car; } return cars; } protected void _ConfigureViews(DHXScheduler scheduler, IEnumerable cars) { var units = new TimelineView( "Orders" , "car_id" ); units.X_Step = 2; units.X_Length = 12; units.X_Size = 12; //width of the first column units.Dx = 149; //row height units.Dy = 76; //order bar height units.EventDy = units.Dy - 5; units.AddOptions(cars); units.RenderMode = TimelineView.RenderModes.Bar; scheduler.Views.Clear(); scheduler.Views.Add(units); scheduler.InitialView = scheduler.Views[0].Name; } protected void _ConfigureLightbox(DHXScheduler scheduler, IEnumerable cars) { scheduler.Lightbox.Add( new LightboxText( "text" , "Contact details" ) { Height = 42, Focus = true }); scheduler.Lightbox.Add( new LightboxText( "description" , "Note" ) { Height = 63 }); var select = new LightboxSelect( "car_id" , "Car Brand" ); scheduler.Lightbox.Add(select); scheduler.Lightbox.Add( new LightboxText( "pick_location" , "Pick up location" ) { Height = 21 }); scheduler.Lightbox.Add( new LightboxText( "drop_location" , "Drop off location" ) { Height = 21 }); select.AddOptions(cars); scheduler.Lightbox.Add( new LightboxTime( "time" , "Time period" )); } public ContentResult Data() { return new SchedulerAjaxData(( new RentalDataContext()).Orders); } public ContentResult Save( int ? id, FormCollection actionValues) { var action = new DataAction(actionValues); RentalDataContext data = new RentalDataContext(); try { var changedEvent = (Order)DHXEventsHelper.Bind( typeof (Order), actionValues); switch (action.Type) { case DataActionTypes.Insert: data.Orders.InsertOnSubmit(changedEvent); break ; case DataActionTypes.Delete: changedEvent = data.Orders.SingleOrDefault(ev => ev.id == action.SourceId); data.Orders.DeleteOnSubmit(changedEvent); break ; default : // "update" var eventToUpdate = data.Orders.SingleOrDefault(ev => ev.id == action.SourceId); DHXEventsHelper.Update(eventToUpdate, changedEvent, new List< string >() { "id" }); break ; } data.SubmitChanges(); action.TargetId = changedEvent.id; } catch { action.Type = DataActionTypes.Error; } return ( new AjaxSaveResponse(action)); } } } |
The calendar with a date filter is ready.
Part III. Signing rent boxes with rent duration period
Customize the template to show the rent duration period in the rent box (e.g. “Rented for 4 hours”). Add the required attributes to scripts.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| //set displayed text var durations = { day: 24 * 60 * 60 * 1000, hour: 60 * 60 * 1000 }; var get_formatted_duration = function (start, end) { var diff = end - start; var days = Math.floor(diff / durations.day); diff -= days * durations.day; var hours = Math.floor(diff / durations.hour); diff -= hours * durations.hour; var results = []; if (days) results.push(days + " days" ); if (hours) results.push(hours + " hours" ); return results.join( ", " ); }; |
And finally, update Index.cshtml to display the duration in the rent box:
1
2
3
| scheduler.templates.event_bar_text = function (start, end, event ) { var text = "Rented" ; return text + " for " + get_formatted_duration(start, end); |
That’s it! Car rental application for ASP.NET MVC3 Razor is ready to use.
0 comments:
Post a Comment