Start Building Professional
Web Apps Today


Gantt Chart for ASP.NET MVC with dhtmlxGantt

May 14th, 2014

This tutorial will lead you through the steps required to integrate dhtmlxGantt into an ASP.NET MVC application. dhtmlxGantt is an open source (GPL) JavaScript library that draws attractive Gantt charts and allows the end users to edit the tasks in a convenient way. We will learn how to put this Ajax-based Gantt chart on a web page, load tasks from the .NET server side, and update them in the database when a user makes changes in the browser.

Gantt Chart for ASP.NET MVC - Demo

If you are familiar with ASP.NET MVC and dhtmlxGantt and you just want to dive into the code, download the final demo app.

NOTE: The sample goes without pre-downloaded NuGet packages. They will be installed automatically when you Build the project in Visual Studio. If it doesn’t happen, you can either configure Visual Studio to restore missing packages during the build, or install them manually by this command in Package Manager Console:

 >> Update-Package-Reinstall

If you need the detailed instructions on how to use dhtmlxGantt with ASP.NET MVC, keep reading the tutorial.

Getting Started

 
First of all, you need to create a new MVC project.

dhtmlxGantt with ASP.NET MVC - Creating a Project

The next things you need are dhtmlxGantt and EntityFramework libraries. You can install them using Nuget Package Manager.

Once all necessary libraries are installed you can proceed to the first step.

Step 1 – Initialization

 
First of all, you need to add links to the dhtmlxGantt files. Open the _Layout.cshtml file and modify it as follows:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title</title>
    <script src="~/Scripts/dhtmlxgantt/dhtmlxgantt.js" type="text/javascript" charset="utf-8"></script>
        <link rel="stylesheet" href="~/Content/dhtmlxgantt/dhtmlxgantt.css" type="text/css" />  
    <style type="text/css">
html, body
{
    height: 100%;
    padding: 0px;
    margin: 0px;
    overflow: hidden;
}
    </style>
</head>
<body>

    @RenderBody()

    <script src="~/Scripts/main.js" type="text/javascript" charset="utf-8"></script>

</body>
</html>

Then create HomeController with Index action. Add this HTML container for dhtmlxGantt to the Index view:

<div id="ganttContainer" style="width: 100%; height: 100%;"></div>

After that we need to add code that initializes the Gantt chart on a page. Create main.js file in the Scripts folder and fill it with the following data:

(function () {
    // add month scale
    gantt.config.scale_unit = "week";
    gantt.config.step = 1;
    gantt.templates.date_scale = function (date) {
        var dateToStr = gantt.date.date_to_str("%d %M");
        var endDate = gantt.date.add(gantt.date.add(date, 1, "week"), -1, "day");
        return dateToStr(date) + " - " + dateToStr(endDate);
    };
    gantt.config.subscales = [
        { unit: "day", step: 1, date: "%D" }
    ];
    gantt.config.scale_height = 50;

    // configure milestone description
    gantt.templates.rightside_text = function (start, end, task) {
        if (task.type == gantt.config.types.milestone) {
            return task.text;
        }
        return "";
    };
    // add section to type selection: task, project or milestone
    gantt.config.lightbox.sections = [
        { name: "description", height: 70, map_to: "text", type: "textarea", focus: true },
        { name: "type", type: "typeselect", map_to: "type" },
        { name: "time", height: 72, type: "duration", map_to: "auto" }
    ];

    gantt.config.xml_date = "%Y-%m-%d %H:%i:%s"; // format of dates in XML
    gantt.init("ganttContainer"); // initialize gantt
})();

In this code we’ve specified scales configuration – two line scale that will display days and weeks. Also we defined labels text for milestones and sections of the task details form. At the end we’ve specified the format of dates for data loading (this is needed to parse the server-side data correctly) and initialized our Gantt chart.

At this point, the running application will look like this (no data yet):

dhtmlxGantt with ASP.NET MVC - Empty Chart

Step 2 – Creating Task and Link Models

 
Now we need to create models to store our tasks and links. In this sample we use EF Code First, so you don’t need to create database tables manually. Let’s create Task and Link classes in Model folder:

public class GanttTask
    {
        public int GanttTaskId { get; set; }
        [MaxLength(255)]
        public string Text { get; set; }
        public DateTime StartDate { get; set; }
        public int Duration { get; set; }
        public decimal Progress { get; set; }
        public int SortOrder { get; set; }
        public string Type { get; set; }
        public int? ParentId { get; set; }
    }

public class GanttLink
    {
        public int GanttLinkId { get; set; }
        [MaxLength(1)]
        public string Type { get; set; }
        public int SourceTaskId { get; set; }
        public int TargetTaskId { get; set; }
    }

Note that classes for Tasks and Links can have any number of custom columns that can be accessed on the client-side.

The next step is to create the database context class. Create a new folder DAL (for Data Access Layer). In that folder create a new class file named GanttContext.cs, and replace the template code with the following code:

public class GanttContext : DbContext
    {
        public GanttContext() : base("GanttContext") { }

        public DbSet<GanttTask> GanttTasks { get; set; }
        public DbSet<GanttLink> GanttLinks { get; set; }
    }

In this class, we link our models with database. The context class will use connection string named “GanttContext”, so you’ll need to define one in order to fetch the database.

Step 3 – Loading Data

 
Now it came the turn of loading data. The client-side dhtmlxGantt component uses simple JSON structure as described here. Basically it is an object with two array properties, one for the links and one for the tasks. Task dates should be serialized in format specified in gantt.config.xml_date, that has been defined in the main.js. We need to create a JSON object with two arrays for Tasks and Links.

Add Data action to HomeController.cs and GanttContext variable to access the database:

// database access
private readonly GanttContext db = new GanttContext();

public JsonResult GetGanttData()
{
            var jsonData = new
            {
                // create tasks array
                data = (
                    from t in db.GanttTasks.AsEnumerable()
                    select new
                    {
                        id = t.GanttTaskId,
                        text = t.Text,
                        start_date = t.StartDate.ToString("u"),
                        duration = t.Duration,
                        order = t.SortOrder,
                        progress = t.Progress,
                        open = true,
                        parent = t.ParentId,
                        type = (t.Type != null) ? t.Type : String.Empty
                    }
                ).ToArray(),
                // create links array
                links = (
                    from l in db.GanttLinks.AsEnumerable()
                    select new
                    {
                        id = l.GanttLinkId,
                        source = l.SourceTaskId,
                        target = l.TargetTaskId,
                        type = l.Type
                    }
                ).ToArray()
            };

            return new JsonResult { Data = jsonData, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}

Then we need to configure dhtmlxGantt to load data from this actions. Add the following line after the grid initialization in the main.js file:

gantt.load("/Home/Data", "json");

Now dhtmlxGantt is able to load data. However our database is empty so let’s configure entity framework to initialize the database with test data. In the DAL folder, create a new class file named GanttInitializer.cs and replace the template code with the following code, which causes a database to be created when needed and loads test data into the new database:

   public class GanttInitializer : DropCreateDatabaseIfModelChanges<GanttContext>
    {
        protected override void Seed(GanttContext context)
        {
            List<GanttTask> tasks = new List<GanttTask>()
            {
                new GanttTask() { GanttTaskId = 1, Text = "Project #2", StartDate = DateTime.Now.AddHours(-3),
                    Duration = 18, SortOrder = 10, Progress = 0.4m, ParentId = null },
                new GanttTask() { GanttTaskId = 2, Text = "Task #1", StartDate = DateTime.Now.AddHours(-2),
                    Duration = 8, SortOrder = 10, Progress = 0.6m, ParentId = 1 },
                new GanttTask() { GanttTaskId = 3, Text = "Task #2", StartDate = DateTime.Now.AddHours(-1),
                    Duration = 8, SortOrder = 20, Progress = 0.6m, ParentId = 1 }
            };

            tasks.ForEach(s => context.GanttTasks.Add(s));
            context.SaveChanges();

            List<GanttLink> links = new List<GanttLink>()
            {
                new GanttLink() { GanttLinkId = 1, SourceTaskId = 1, TargetTaskId = 2, Type = "1" },
                new GanttLink() { GanttLinkId = 2, SourceTaskId = 2, TargetTaskId = 3, Type = "0" }
            };

            links.ForEach(s => context.GanttLinks.Add(s));
            context.SaveChanges();
        }
    }

The Seed method takes the database context object as an input parameter, and the code in the method uses
that object to add new entities to the database. For each entity type, the code creates a collection of new entities, adds them to the appropriate DbSet property, and then saves the changes to the database.

To tell Entity Framework to use your initializer class, add an element to the entityFramework element in the application Web.config file, as shown in the following example:

<contexts>
    <context type="Gantt.DAL.GanttContext, Gantt">
        <databaseInitializer type="Gantt.DAL.GanttInitializer, Gantt" />
    </context>
</contexts>

dhtmlxGantt is now ready for loading data. If you run your application, it will look like this:

dhtmlxGantt with ASP.NET MVC - Gantt Chart with Data

Step 4 – Saving Changes to the Database

 
After all the steps described above, we’ve built a Gantt chart that loads the tasks from the database but that’s not enough. Our next goal is to save changes made on the client side to the server. For this we will use dataProcessor on the client. This library is integrated into dhtmlxGantt and automatically traces the changes made on the client side and sends updates to the server, so we need to configure the server to handle these requests.

First of all, we need a class which will parse and represent data action sent from the client. Create a new GanttRequest model in the Model folder and add the following code:

public enum GanttMode
    {
        Tasks,
        Links
    }

public enum GanttAction
    {
        Inserted,
        Updated,
        Deleted,
        Error
    }

public static List<GanttData> Parse(FormCollection form, string ganttMode)
        {
            // save current culture and change it to InvariantCulture for data parsing
            var currentCulture = Thread.CurrentThread.CurrentCulture;
            Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;

            var ganttDataCollection = new List<GanttData>();
            var prefixes = form["ids"].Split(',');

            foreach (var prefix in prefixes)
            {
                var ganttData = new GanttData();

                // lambda expression for form data parsing
                Func<string, string> parse = x => form[String.Format("{0}_{1}", prefix, x)];

                ganttData.Mode = (GanttMode)Enum.Parse(typeof(GanttMode), ganttMode, true);
                ganttData.Action = (GanttAction)Enum.Parse(typeof(GanttAction), parse("!nativeeditor_status"), true);
                ganttData.SourceId = Int64.Parse(parse("id"));

                // parse gantt task
                if (ganttData.Action != GanttAction.Deleted && ganttData.Mode == GanttMode.Tasks)
                {
                    ganttData.GanttTask = new GanttTask()
                    {
                        GanttTaskId = (ganttData.Action == GanttAction.Updated) ? (int)ganttData.SourceId : 0,
                        Text = parse("text"),
                        StartDate = DateTime.Parse(parse("start_date")),
                        Duration = Int32.Parse(parse("duration")),
                        Progress = Decimal.Parse(parse("progress")),
                        ParentId = (parse("parent") != "0") ? Int32.Parse(parse("parent")) : (int?)null,
                        SortOrder = (parse("order") != null) ? Int32.Parse(parse("order")) : 0,
                        Type = parse("type")
                    };
                }
                // parse gantt link
                else if (ganttData.Action != GanttAction.Deleted && ganttData.Mode == GanttMode.Links)
                {
                    ganttData.GanttLink = new GanttLink()
                    {
                        GanttLinkId = (ganttData.Action == GanttAction.Updated) ? (int)ganttData.SourceId : 0,
                        SourceTaskId = Int32.Parse(parse("source")),
                        TargetTaskId = Int32.Parse(parse("target")),
                        Type = parse("type")
                    };
                }

                ganttDataCollection.Add(ganttData);
            }

            // return current culture back
            Thread.CurrentThread.CurrentCulture = currentCulture;

            return ganttDataCollection;
        }

GanttRequest class has several properties to store the action info that came from the client side, and one method that creates a collection of data actions from the request values.

The data is sent from the client in the following format:

ids:13,
13_!nativeeditor_status:"inserted",
13_text: "New task"
13_duration: 1

which is common for all DHTMLX components that use dhtmlxDataprocessor.

A single request may specify operations on several data items. Each property has prefix that links to an id of the related data item. All sent ids are stored in “ids” parameter and separated by commas:

ids:13,25
13_!nativeeditor_status:inserted,
25_!nativeeditor_status:updated,
...

By default, requests for insert/update contains only one id, while requests for deleting may have multiple items specified. Although, in general case, the request may contain any number of operations of different kind.

First, we change the current culture to InvariantCulture. This is needed for more predictable parsing of request parameters – the format of dates and numbers that comes from a client-side component does not depends on the server-side culture settings. Then we parse request values into collection of individual data actions and store it in request variable (GanttRequest class has been defined in Models/GanttRequest.cs).

Note that the type of data action is defined by two properties:

  • Mode – specifies the data entity, it can be a link or a task
  • Actions – specifies the type of an operation, can be delete, update, insert

Depending on action and mode, we populate UpdatedTask or UpdatedLink objects from the request values.

Now, depending on the action, we need to save the changes. For this, we will create in HomeController.cs Save action, two private methods: UpdateTasks and UpdateLinks. We also create GanttRespose method for XML response.

      [HttpPost]
        public ContentResult UpdateGanttData(FormCollection form)
        {
            var ganttDataCollection = GanttData.Parse(form, Request.QueryString["gantt_mode"]);
            try
            {
                foreach (var ganttData in ganttDataCollection)
                {
                    switch (ganttData.Mode)
                    {
                        case GanttMode.Tasks:
                            UpdateGanttTasks(ganttData);
                            break;
                        case GanttMode.Links:
                            UpdateGanttLinks(ganttData);
                            break;
                    }
                }
                db.SaveChanges();
            }
            catch
            {
                // return error to client if something went wrong
                ganttDataCollection.ForEach(g => { g.Action = GanttAction.Error; });
            }
            return GanttRespose(ganttDataCollection);
        }

        private void UpdateGanttTasks(GanttData ganttData)
        {
            switch (ganttData.Action)
            {
                case GanttAction.Inserted:
                    // add new gantt task entity
                    db.GanttTasks.Add(ganttData.GanttTask);
                    break;
                case GanttAction.Deleted:
                    // remove gantt tasks
                    db.GanttTasks.Remove(db.GanttTasks.Find(ganttData.SourceId));
                    break;
                case GanttAction.Updated:
                    // update gantt task
                    db.Entry(db.GanttTasks.Find(ganttData.GanttTask.GanttTaskId)).CurrentValues.SetValues(ganttData.GanttTask);
                    break;
                default:
                    ganttData.Action = GanttAction.Error;
                    break;
            }
        }

        private void UpdateGanttLinks(GanttData ganttData)
        {
            switch (ganttData.Action)
            {
                case GanttAction.Inserted:
                    // add new gantt link
                    db.GanttLinks.Add(ganttData.GanttLink);
                    break;
                case GanttAction.Deleted:
                    // remove gantt link
                    db.GanttLinks.Remove(db.GanttLinks.Find(ganttData.SourceId));
                    break;
                case GanttAction.Updated:
                    // update gantt link
                    db.Entry(db.GanttLinks.Find(ganttData.GanttLink.GanttLinkId)).CurrentValues.SetValues(ganttData.GanttLink);
                    break;
                default:
                    ganttData.Action = GanttAction.Error;
                    break;
            }
        }

       private ContentResult GanttRespose(List<GanttData> ganttDataCollection)
        {
            var actions = new List<XElement>();
            foreach (var ganttData in ganttDataCollection)
            {
                var action = new XElement("action");
                action.SetAttributeValue("type", ganttData.Action.ToString().ToLower());
                action.SetAttributeValue("sid", ganttData.SourceId);
                action.SetAttributeValue("tid", (ganttData.Action != GanttAction.Inserted) ? ganttData.SourceId :
                    (ganttData.Mode == GanttMode.Tasks) ? ganttData.GanttTask.GanttTaskId : ganttData.GanttLink.GanttLinkId);
                actions.Add(action);
            }

            var data = new XDocument(new XElement("data", actions));
            data.Declaration = new XDeclaration("1.0", "utf-8", "true");
            return Content(data.ToString(), "text/xml");
        }

In the Save action we parse the request values into collection of individual data actions. Then for each data action, depending on it’s mode, we perform update on links or tasks table.

After updates are done, we need to return the response for the client side. Method GanttRespose renders the XML response that will notify the client-side about the result of operation (success or error).

UpdateGanttTaks and UpdateLinks methods are fairly simple. They call Entity Framework function to update/insert/delete new tasks or links.

XML response format is also very simple:

insert: <data><action type="inserted" sid="temp id" tid="new id from database" /></data>
update: <data><action type="updated" sid="entity id" tid="entity database" /></data>
delete: <data><action type=”deleted” sid=”first entity id” tid=”first entity id” /><action type=”deleted” sid=”second entity id” tid=”second entity id” /></data>

We are almost done. The last thing left to do is to activate the dataProcessor on the client. Add the following lines to the end of the main.js file:

// enable dataProcessor
var dp = new dataProcessor("/Home/Save");
dp.init(gantt);

Now, if you run the application after these updates, you can create/update/delete tasks and links on your Gantt chart.

dhtmlxGantt with ASP.NET MVC - Final Demo

If you followed this tutorial, you now have built an interactive Gantt chart with ASP.NET MVC backend. The end users can create and manage tasks using an intuitive drag-and-drop interface. All the changes are saved to the server and updated in the database. Using dhtmlxGantt API, you can extend the functionality of your app and customize it further to your needs. Hope, this basic example will be a good help for you.

Download the final demo app to better understand this integration of dhtmlxGantt with ASP.NET MVC.

Comments

  1. Stephen S.,
    June 19, 2014 at 9:34 pm

    Hello all, i just downloaded the standard version of DhtmlxGantt, I am trying to incorporate it in my ASP.Net MVC application and it is not working.

    I followed the tutorial with every step and it doesn’t seem to work.
    It is a MVC5 application using the Razor view engine.

    I do have some other stuff on the same page as the Gantt, but even when trying to put it on its seperate page, it does not show up.

    when I tried the sample file provided for download, it did work.

    • Craig cocker,
      June 25, 2014 at 8:55 am

      Yes – I have just tried the same thing with MVC 5 application and it does not seem to compile the GanntRequest.

      Also when downloading the finished app – it fails to load. I have tried it in VS2012 & 2013. I also tried the install-package-reinstall but that failed also

Leave a Reply