Our company grows forest seedlings for our clients and then delivers to them. The delivering process is called trucking or shipping, which is a very complicated process when we have huge quantities of seedlings for lots of clients. So I made an event calendar in 2015, using the free Microsoft Visual Studio Community 2015 and jQuery.Fullcalendar
by Adam Shaw, for managing this process online, and for our clients to check their seedlings shipping status online. It is recently put to use. Now I'd like to share my experience and some of my codes, by building a new, simple event calendar from scratch, step by step, to a working calendar, using trucking or shipping as an example.
AJSON has an excellent article Full calendar – A complete web diary system for jQuery and C# MVC, on how to use the jQuery plugin. Except he made it too complicated and did not quite observe the MVC rule. I will explain these when I get there.
Here are the usual steps:
- First, let's use Visual Studio Community 2015, create a new ASP.NET Web Application, and call it "
TruckingCalendar
". Choose MVC and click OK. A blank Trucking Calendar application is created.
- We need a database to hold the trucking events. So the free Microsoft SQL Server 2014 Express is used to create the database, called "
Trucking
". - Create a table in the database, and call it "
Trucking
" too. Add columns: TruckID
(int, Primary Key, Identity Specification = Yes), TruckDate
(smalldatetime
), LoadMinutes
(smallint
), TruckStatus
(tinyint
). Not allow null
s. Default LoadMinutes
to 60
(or any number) and TruckStatus
to 0
.
- In the project, right click Models folder -> Add New Item -> Data -> ADO.NET Entity Data Model, and call it "TruckingModel". Click Add.
- Then choose "EF Designer from database". Click Next. Then in Server name: type "
./SQLEXPRESS
", and in Database name: select the "Trucking
" database that we just created. And Test Connection to show connected. - Click Next. It will create "
TruckingEntities
". Click Next to Choose Your Database Objects and Settings: Choose the Trucking
table, and keep the default settings. - Click Finish and we have got a TruckingModel.edmx.
- Now we need the
jQuery.Fullcalendar
package. Click Project -> Manage Nuget Packages..., and update all your packages already there. - Then browse and install
jQuery.Fullcalendar
, and Moment.js, since Fullcalendar
uses Moment.js for DateTime
settings. - Open BundleConfig.cs in App_Start folder. In the last bundles (the CSS bundle), add "~/Content/themes/base/all.css". Check your folder, see if the file is there. These CSS themes will be used by the
Calendar
. - Now we get to do the fun staff: the EF scaffolding. But do a project rebuild first. Then right click Controllers folder -> Add -> New scaffolded item..., select "MVC 5 Controller with views, using Entity Framework". Click Add.
- In the
Model
class:, select Trucking
, and in the data context class:, select TruckingEntities
, keep the defaults. And click Add.
That's the whole setup, folks. Phew! It did take a lot of work, even for the easy part. Now is the hard part.
Now that the Trucking Controller is open, we are going to replace this:
public ActionResult Index()
{
return View(db.Truckings.ToList());
}
with this:
public ActionResult Calendar(DateTime? truckDate)
{
ViewBag.TruckDate = truckDate ?? DateTime.Now;
return View();
}
ViewBag.TruckDate
is used to get the nullable truckDate
. If it is null
, "Now
" will be used. The purpose will be explained later.
Now we have to rename Index.cshtml in Truckings folder to Calendar.cshtml, according to the Controller
change. Inside the View
, change ViewBag.Title
to "Trucking Calendar
", and replace everything below it with the following 4 blocks of code (because the code is too long for 1 block):
<link rel='stylesheet' href='~/Content/fullcalendar.css' />
<script src='~/Scripts/moment.js'></script>
<script src='~/Scripts/fullcalendar.js'></script>
We can see that above is where the installed packages (Fullcalendar
and Moments
) get used. Codes continue...
<script>
$(document).ready(function () {
$('#calendar').fullCalendar({
header: {
right: 'prev,next today',
center: 'title',
left: 'month,agendaWeek,agendaDay'
},
theme: true,
timezone: 'local',
editable: true,
defaultDate: '@ViewBag.TruckDate',
allDaySlot: false,
selectable: true,
slotMinutes: 15,
events: '@Url.Action("GetEvents")',
The above is setup and automatic actions. Codes continue...
eventClick: function(calEvent) {
window.location='/Truckings/Details/' + calEvent.id;
},
dayClick: function (date, jsEvent, view) {
if(view.name != 'month') {
if (date > moment()) {
if (confirm("Add a new trucking?")) {
window.location = '@Url.Action("Create")' + '?truckDate=' + date.format();
}
}
}
else {
$('#calendar').fullCalendar('changeView', 'agendaDay');
$('#calendar').fullCalendar('gotoDate', date);
}
},
eventDrop: function(event, delta, revertFunc) {
if (confirm("Confirm move?")) {
UpdateEvent(event.id, event.start, event.end);
}
else {
revertFunc();
}
},
eventResize: function (event, delta, revertFunc) {
if (confirm("Confirm change loading time?")) {
UpdateEvent(event.id, event.start, event.end);
}
else {
revertFunc();
}
}
});
});
The above are human interactions. Codes continue...
function UpdateEvent(id, start, end) {
var obj = { id: id, start: start, end: end }
$.ajax({
type: 'POST',
url: '@Url.Action("UpdateEvent")',
dataType: "json",
contentType: "application/json",
data: JSON.stringify(obj)
});
}
</script>
After the script section, add: <div id='calendar'></div>
to hold the Calendar. And that is the end of the code in Calendar.cshtml.
To link to the Calendar
, open the Index.cshtml in Home folder, keep the "Home Page
" title, but replace everything else with this action link:
<div class="jumbotron h3 text-center">
@Html.ActionLink("Trucking Calendar", "Calendar", "Truckings", null, null)
</div>
Now run the project, click the Trucking Calendar link on the Home page, we get a blank Calendar
.
Yay! If you get this far, congratulate yourself!
Now we need to use the database to input and output events.
First, let's make a ViewModel
. Right click Models folder, add a Class
, and call it TruckingVMs.cs. When it is open, replace the public class TruckingVMs
with this code:
public class Events
{
public int id;
public string title;
public DateTime start;
public DateTime end;
public string color;
}
We also need an Enum
for TruckStatus
, remembering its data type is tinyint
. So right click Models folder, add a Class
, and call it Enums.cs. When it is open, replace the generated code with this code:
using System.ComponentModel.DataAnnotations;
namespace TruckingCalendar.Models
{
public enum TruckStatus : byte
{
[Display(Name = "Planned")]
orange,
[Display(Name = "Confirmed")]
green,
[Display(Name = "Changed")]
red,
[Display(Name = "Loaded")]
darkcyan
}
}
So the TruckStatus
will have 4 status with 4 different colors, but stay in database as 0, 1, 2, 3.
Then click the model EvergreenModel.edmx, and the database diagram will show. Right click TruckStatus
in the Trucking
table -> Convert to Enum
. Next, in Enum
Type Name:
, enter TruckStatus
. Click Reference external type, and enter TruckingModel.TruckStatus
. Click OK to convert. Now check the Trucking.cs partial class, it will show: public TruckingModel.TruckStatus TruckStatus { get; set; }
. Somehow, the "TruckingModel.
" is not allowed here, although necessary in the edmx. It is a glitch in Visual Studio 2015. So we have to delete the "TruckingModel.
" from Trucking.cs.
Now we also need a display template and an editor template to display and edit the enum
. Shahriar Hossain has an excellent article on how to make them: Dealing with Enum in MVC 5.1. So make those 2 templates like he shows.
Also add a DateTime.cshtml in the EditorTemplates folder, and replace the generated code with this:
@model Nullable
@{
DateTime dt = DateTime.Now;
if (Model != null)
{
dt = (System.DateTime)Model;
}
@Html.TextBox("", String.Format("{0:d}", dt.ToShortDateString() + ' ' +
dt.ToShortTimeString()), new { @class = "form-control datecontrol", type = "datetime" })
}
This will set the DateTime
to local time with a Time Zone (with the timezone: 'local
' directive in the Calendar.cshtml), when we add a new event.
Eventually, we are ready to do some input and output. First, input. Open TruckingsController
, go to public ActionResult Create()
, change the generated code to:
public ActionResult Create(DateTime truckDate)
{
ViewBag.TruckDate = truckDate;
return View();
}
The truckDate
here comes from the following code of the Calendar.cshtml, as shown before:
dayClick: function (date, jsEvent, view) {
if(view.name != 'month') {
if (date > moment()) {
if (confirm("Add a new trucking?")) {
window.location = '@Url.Action("Create")' + '?truckDate=' + date.format();
}
}
}
else {
$('#calendar').fullCalendar('changeView', 'agendaDay');
$('#calendar').fullCalendar('gotoDate', date);
}
}
When you click a future time in the Calendar
, the Calendar
will ask you if you want to add a new trucking event. If you click OK, the Calendar
will send you to the Controller's Create
action, with the truckDate
(DateTime
) you just clicked. So the ViewBag.TruckDate
has a selected value now, and we want to add the selected value to the Create.cshtml:
@Html.EditorFor(model => model.TruckDate,
new { htmlAttributes = new { @class = "form-control", @Value = ViewBag.TruckDate } })
Then go back to TruckingController
's POST
: Truckings/Create section, change:
return RedirectToAction("Index");
to:
return RedirectToAction("Calendar", new { truckDate = trucking.TruckDate });
This truckDate
is redirected to Calendar
action, and sent to Calendar View as defaultDate
: '@ViewBag.TruckDate
', so that the Calendar
goes back to the month it come from. Many developers asked how to go back to the Calendar
month where they left, this is the way to do it.
Do the same for the RedirectToAction
in Edit
and Delete
section.
Then, go to the Trucking Views, at the end of each View, change:
@Html.ActionLink("Back to List", "Index")
to:
@Html.ActionLink("Back to Calendar", "Calendar", new { truckDate = Model.TruckDate })
Except for Create.cshtml, we have to use "ViewBag.TruckDate
", which we already used for its selected value. So that they can all go back to where they come from when "Back to Calendar" is clicked.
OK. This is the input. What about the output? Ah..., just a moment, let me take a break...
Ok. Here we go again. First, let's look at the Calendar View: events: '@Url.Action("GetEvents")
', which means, when the Calendar
opens, it will go to the TruckingsController
's GetEvents
action to get events.
So we have to open the TruckingsController
and add the GetEvents
action at the end:
public JsonResult GetEvents(DateTime start, DateTime end)
{
var truckings = (from t in db.Truckings
where t.TruckDate >= start &&
DbFunctions.AddMinutes(t.TruckDate, t.LoadMinutes) <= end
select new { t.TruckID, t.TruckDate, t.LoadMinutes, t.TruckStatus }).ToList()
.Select(x => new Events()
{
id = x.TruckID,
title = "Trucking " + x.TruckID + ", " + x.LoadMinutes + "min ",
start = x.TruckDate,
end = x.TruckDate.AddMinutes(x.LoadMinutes),
color = Enum.GetName(typeof(TruckStatus), x.TruckStatus)
}).ToArray();
return Json(truckings, JsonRequestBehavior.AllowGet);
}
The start and end come from the Calendar
, depending on it is a month, week or day, it will get events in that time period. The events then are sent to the ViewModel
in this step: x => new Events()
, and from the ViewModel
to the Calendar
, all in one action, and one place. And getting color is just one line of code. Boom! It is there.
In AJSON's project, he puts some controller action in his ViewModel
, so that the controller has to call the ViewModel
to do the calculation, then come back with the data, and the data has to transform several times before send to the Calendar
. Not very efficient. Also, his way of getting the color is complicated and difficult.
Now, let's look more at the Calendar View:
eventClick: function(calEvent) {
window.location='/Truckings/Details/' + calEvent.id;
},
It means, if we click a trucking event, we will see the details of the event. From there, we can come back, or do some editing and come back. Nice.
We can also do some drag and drop of the events on the Calendar
, as indicated by eventDrop:
and eventResize:
, and they all call for UpdateEvent
function:
function UpdateEvent(id, start, end) {
var obj = { id: id, start: start, end: end }
$.ajax({
type: 'POST',
url: '@Url.Action("UpdateEvent")',
dataType: "json",
contentType: "application/json",
data: JSON.stringify(obj)
});
}
And the UpdateEvent
in turn calls for the TruckingsController
's UpdateEvent
action. So we need to add that action to the end of the Controller
:
public void UpdateEvent(int id, DateTime start, DateTime end)
{
Trucking trucking = db.Truckings.Find(id);
trucking.TruckDate = start;
trucking.LoadMinutes = (short)(end - start).TotalMinutes;
db.SaveChanges();
}
That's it folks! We are done! Run your project, click the link to the calendar. Click a future time and add some events; change status to see the color change; drag and drop the events to different time, date, week or month; click an event to see details; do some editing and deleting; click Back to Calendar to go back to original month. Here is my screen shot:
You should see something like this too.
Now it's time to celebrate!! Oh, wait..., you got questions? Suggestions? Thoughts? Check my GitHub repository: A complete event calendar, using trucking/shipping as an example.
References