This tutorial gives you step-by-step instructions on how to create Gantt with ASP.NET Core on the server side.
You can also read tutorials on other server-side technologies:
To organize communication with database, the Entity Framework Core is used. The application is built with the help of the Visual Studio 2022.
The complete source code is available on GitHub.
Launch Visual Studio 2022 and create a new project. Select: Create a new project.
Next select "ASP.NET Core Web App" and name it DHX.Gantt.
Thus you've created a project and can proceed to add markup and script for Gantt.
Go to wwwroot and create an index.html file.
In the newly created file make a simple page for a gantt chart.
Note, that gantt files are added from CDN in this demo. If you have a Professional version of the component, you'll need to add gantt files to your project manually.
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
<link href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css"
rel="stylesheet" type="text/css" />
<script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function(event) {
// specifying the date format
gantt.config.date_format = "%Y-%m-%d %H:%i";
// initializing gantt
gantt.init("gantt_here");
// initiating data loading
gantt.load("/api/data");
// initializing dataProcessor
var dp = new gantt.dataProcessor("/api/");
// and attaching it to gantt
dp.init(gantt);
// setting the REST mode for dataProcessor
dp.setTransactionMode("REST");
});
</script>
</head>
<body>
<div id="gantt_here" style="width: 100%; height: 100vh;"></div>
</body>
</html>
When the page is loaded, in addition to initializing gantt chart data loading is immediately called and the
dataProcessor
is set up, so all changes made to gantt chart by the user will be saved to the backend. The backend isn't implemented yet, so it will make more sense later.
Next go to Program.cs and tell the application to use the index.html page. In order to do so, you need to configure the app to serve static files from the wwwroot
folder.
For this, you need to add the app.UseDefaultFiles()
method.
You can find more details here.
Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days.
// You may want to change this for production scenarios,
// see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
The app.UseDefaultFiles()
method allows serving default files. It will search the wwwroot folder for the following files:
Thus, you can choose any of them, while in this tutorial "index.html" is used.
UseDefaultFiles()
is just an URL-rewriter that doesn't actually serve the file. For this purpose you need to also add the UseStaticFiles()
file.
Once you are done with it, an empty gantt should appear on the page when you run the application. Note that the "Invalid data" label at the top right corner shows up because gantt.load()
is called,
as there is still no proper backend to serve the data. When the controller will be implemented, gantt will be able to display tasks and links.
Now the basic part is done and it's time to implement the backend. Let's start with implementing model classes and after that proceed to the WebAPI controller.
Let's begin with data models. A data model for Gantt includes links and tasks. dhtmlxGantt uses non-conventional names for model properties from the .NET world perspective. Sometimes the client-side model also contains some properties for the client side or the backend logic, but these properties shouldn't be stored in a database.
To deal with this, the Data Transfer Object (DTO) pattern will be used. Two kinds of models will be defined:
Then mapping between the two models should be implemented.
Create a new folder called Models in the project folder. This is where model classes and EF context will be implemented.
First, create a class for Tasks. Create a file in the Models folder and name it Task.cs. This can be done by calling the context menu for the Models folder and selecting Add->Class.
This is how the model must look like:
DHX.Gantt/Models/Task.cs
namespace DHX.Gantt.Models
{
public class Task
{
public int Id { get; set; }
public string? Text { get; set; }
public DateTime StartDate { get; set; }
public int Duration { get; set; }
public decimal Progress { get; set; }
public int? ParentId { get; set; }
public string? Type { get; set; }
}
}
You can look up the list of all properties of the Task object.
Add one more file and create a class for Links:
DHX.Gantt/Models/Link.cs
namespace DHX.Gantt.Models
{
public class Link
{
public int Id { get; set; }
public string? Type { get; set; }
public int SourceTaskId { get; set; }
public int TargetTaskId { get; set; }
}
}
The models are ready, and you can start configuring the database connection.
To configure database connection, you need to take the steps listed below:
The Entity Framework Core will be used to manage communication of the app with a database. Let's install the framework:
Or use the Package Manager command line:
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore
PM> Install-Package Microsoft.EntityFrameworkCore.Design
The Entity Framework Core will be used to manage communication of the app with a database.
Next you need to define a session with the database and enable loading and saving data. For this, create Context:
DHX.Gantt/Models/GanttContext.cs
using Microsoft.EntityFrameworkCore;
namespace DHX.Gantt.Models
{
public class GanttContext : DbContext
{
public GanttContext(DbContextOptions<GanttContext> options)
: base(options)
{
}
public DbSet<Task> Tasks { get; set; } = null;
public DbSet<Link> Links { get; set; } = null;
}
}
Now you can add records to the database. Let's create the database initializer that will populate the database with tasks. In the Models folder define a class and call it GanttSeeder. The class will have the Seed() method that will add tasks and links to the database.
DHX.Gantt/Models/GanttSeeder.cs
using Microsoft.EntityFrameworkCore;
namespace DHX.Gantt.Models
{
public static class GanttSeeder
{
public static void Seed(GanttContext context)
{
if (context.Tasks.Any())
{
return; // DB has been seeded
}
using (var transaction = context.Database.BeginTransaction())
{
List<Task> tasks = new List<Task>()
{
new Task()
{
Id = 1,
Text = "Project #2",
StartDate = DateTime.Today.AddDays(-3),
Duration = 18,
Progress = 0.4m,
ParentId = null
},
new Task()
{
Id = 2,
Text = "Task #1",
StartDate = DateTime.Today.AddDays(-2),
Duration = 8,
Progress = 0.6m,
ParentId = 1
},
new Task()
{
Id = 3,
Text = "Task #2",
StartDate = DateTime.Today.AddDays(-1),
Duration = 8,
Progress = 0.6m,
ParentId = 1
}
};
tasks.ForEach(s => context.Tasks.Add(s));
context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Tasks ON;");
context.SaveChanges();
context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Tasks OFF;");
List<Link> links = new List<Link>()
{
new Link() {Id = 1, SourceTaskId = 1, TargetTaskId = 2, Type = "1"},
new Link() {Id = 2, SourceTaskId = 2, TargetTaskId = 3, Type = "0"}
};
links.ForEach(s => context.Links.Add(s));
context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Links ON;");
context.SaveChanges();
context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Links OFF;");
transaction.Commit();
}
}
}
}
Now you should register the database in Program.cs. But first you need a connection string for it. It will be stored in a JSON file in the application settings . Create the appsettings.json file (or open it if you have it already) and add a connection string to the database:
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;
Database=GanttDatabase;Trusted_Connection=True;"
}
}
The database context will be registered via dependency injection.
Add the following namespaces to Program.cs:
Program.cs
using Microsoft.EntityFrameworkCore;
using DHX.Gantt.Models;
The declaration will look like this:
Program.cs
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<GanttContext>(
options => options.UseSqlServer(connectionString));
To enable controllers, the services.AddControllers() method is called:
Program.cs
builder.Services.AddControllers();
And we call app.MapControllers() to register our controller routes:
Program.cs
app.MapControllers();
Here is the complete code of Program.cs:
Program.cs
using Microsoft.EntityFrameworkCore;
using DHX.Gantt.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<GanttContext>(
options => options.UseSqlServer(connectionString));
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days.
// You may want to change this for production scenarios,
// see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
Finally, you need to initialize and seed the database on the app startup. Normally, you'd want to use migrations for that, but for simplicity they aren't used here.
Let's begin with creating a class where initialization will be done. Create the GanttInitializerExtension.cs file in the Models folder:
Models/GanttInitializerExtension.cs
namespace DHX.Gantt.Models
{
public static class GanttInitializerExtension
{
public static IHost InitializeDatabase(this IHost webHost)
{
var serviceScopeFactory =
(IServiceScopeFactory?)webHost.Services.GetService(typeof(IServiceScopeFactory));
using (var scope = serviceScopeFactory!.CreateScope())
{
var services = scope.ServiceProvider;
var dbContext = services.GetRequiredService<GanttContext>();
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
GanttSeeder.Seed(dbContext);
}
return webHost;
}
}
}
Next call InitializeDatabase():
Program.cs
app.InitializeDatabase();
As it was mentioned above, migrations aren't used in this tutorial. Instead simple EnsureCreated and seed are used.
The current part is finished, let's return to Gantt.
It is high time to define DTO classes that will be used for Web API. Let's begin with the DTO class for Task. In the Models folder create a file and define the WebApiTask.cs class:
Models/WebApiTask.cs
namespace DHX.Gantt.Models
{
public class WebApiTask
{
public int id { get; set; }
public string? text { get; set; }
public string? start_date { get; set; }
public int duration { get; set; }
public decimal progress { get; set; }
public int? parent { get; set; }
public string? type { get; set; }
public bool open
{
get { return true; }
set { }
}
public static explicit operator WebApiTask(Task task)
{
return new WebApiTask
{
id = task.Id,
text = task.Text,
start_date = task.StartDate.ToString("yyyy-MM-dd HH:mm"),
duration = task.Duration,
parent = task.ParentId,
type = task.Type,
progress = task.Progress
};
}
public static explicit operator Task(WebApiTask task)
{
return new Task
{
Id = task.id,
Text = task.text,
StartDate = task.start_date != null ? DateTime.Parse(task.start_date,
System.Globalization.CultureInfo.InvariantCulture) : new DateTime(),
Duration = task.duration,
ParentId = task.parent,
Type = task.type,
Progress = task.progress
};
}
}
}
And this is the DTO class for Link defined in the file called WebApiLink.cs in the Models folder:
Models/WebApiLink.cs
namespace DHX.Gantt.Models
{
public class WebApiLink
{
public int id { get; set; }
public string? type { get; set; }
public int source { get; set; }
public int target { get; set; }
public static explicit operator WebApiLink(Link link)
{
return new WebApiLink
{
id = link.Id,
type = link.Type,
source = link.SourceTaskId,
target = link.TargetTaskId
};
}
public static explicit operator Link(WebApiLink link)
{
return new Link
{
Id = link.id,
Type = link.type,
SourceTaskId = link.source,
TargetTaskId = link.target
};
}
}
}
When you finish this step, you should get the following folder structure:
Now you can run the app in order to check that everything is in place. If you don't see a runtime error, then everything is fine.
Now it's time for the actual REST API implementation.
Create the Controllers folder and create three empty API Controllers: one for Tasks, another for Links and one more for the whole dataset:
Let's create a controller for Tasks. It will define basic CRUD operations for Gantt tasks.
How it works:
Controllers/TaskController.cs
using Microsoft.AspNetCore.Mvc;
using DHX.Gantt.Models;
namespace DHX.Gantt.Controllers
{
[Produces("application/json")]
[Route("api/task")]
public class TaskController : Controller
{
private readonly GanttContext _context;
public TaskController(GanttContext context)
{
_context = context;
}
// GET api/task
[HttpGet]
public IEnumerable<WebApiTask> Get()
{
return _context.Tasks
.ToList()
.Select(t => (WebApiTask)t);
}
// GET api/task/5
[HttpGet("{id}")]
public Models.Task? Get(int id)
{
return _context
.Tasks
.Find(id);
}
// POST api/task
[HttpPost]
public ObjectResult Post(WebApiTask apiTask)
{
var newTask = (Models.Task)apiTask;
_context.Tasks.Add(newTask);
_context.SaveChanges();
return Ok(new
{
tid = newTask.Id,
action = "inserted"
});
}
// PUT api/task/5
[HttpPut("{id}")]
public ObjectResult? Put(int id, WebApiTask apiTask)
{
var updatedTask = (Models.Task)apiTask;
var dbTask = _context.Tasks.Find(id);
if (dbTask == null)
{
return null;
}
dbTask.Text = updatedTask.Text;
dbTask.StartDate = updatedTask.StartDate;
dbTask.Duration = updatedTask.Duration;
dbTask.ParentId = updatedTask.ParentId;
dbTask.Progress = updatedTask.Progress;
dbTask.Type = updatedTask.Type;
_context.SaveChanges();
return Ok(new
{
action = "updated"
});
}
// DELETE api/task/5
[HttpDelete("{id}")]
public ObjectResult DeleteTask(int id)
{
var task = _context.Tasks.Find(id);
if (task != null)
{
_context.Tasks.Remove(task);
_context.SaveChanges();
}
return Ok(new
{
action = "deleted"
});
}
}
}
Next you should create a controller for Links:
Controllers/LinkController.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using DHX.Gantt.Models;
namespace DHX.Gantt.Controllers
{
[Produces("application/json")]
[Route("api/link")]
public class LinkController : Controller
{
private readonly GanttContext _context;
public LinkController(GanttContext context)
{
_context = context;
}
// GET api/Link
[HttpGet]
public IEnumerable<WebApiLink> Get()
{
return _context.Links
.ToList()
.Select(t => (WebApiLink)t);
}
// GET api/Link/5
[HttpGet("{id}")]
public Link? Get(int id)
{
return _context
.Links
.Find(id);
}
// POST api/Link
[HttpPost]
public ObjectResult Post(WebApiLink apiLink)
{
var newLink = (Link)apiLink;
_context.Links.Add(newLink);
_context.SaveChanges();
return Ok(new
{
tid = newLink.Id,
action = "inserted"
});
}
// PUT api/Link/5
[HttpPut("{id}")]
public ObjectResult Put(int id, WebApiLink apiLink)
{
var updatedLink = (Link)apiLink;
updatedLink.Id = id;
_context.Entry(updatedLink).State = EntityState.Modified;
_context.SaveChanges();
return Ok(new
{
action = "updated"
});
}
// DELETE api/Link/5
[HttpDelete("{id}")]
public ObjectResult DeleteLink(int id)
{
var Link = _context.Links.Find(id);
if (Link != null)
{
_context.Links.Remove(Link);
_context.SaveChanges();
}
return Ok(new
{
action = "deleted"
});
}
}
}
Finally, you need to create a controller for a data action:
Controllers/DataController.cs
using Microsoft.AspNetCore.Mvc;
using DHX.Gantt.Models;
namespace DHX.Gantt.Controllers
{
[Produces("application/json")]
[Route("api/data")]
public class DataController : Controller
{
private readonly GanttContext _context;
public DataController(GanttContext context)
{
_context = context;
}
// GET api/data
[HttpGet]
public object Get()
{
return new
{
data = _context.Tasks.ToList().Select(t => (WebApiTask)t),
links = _context.Links.ToList().Select(l => (WebApiLink)l)
};
}
}
}
Everything is ready. You can run the application and see the fully-fledged Gantt.
You can also view the full source code on GitHub.
In order to handle errors, you need to declare a special middleware class that will capture runtime exceptions and write responses. Next it will be added to the app request pipeline. Follow the steps below:
1. Create a middleware class from a template in the project folder.
2. Install the JSON framework for ASP.NET Core. You can either do it via the NuGet package manager:
Or use the Package Manager command line:
PM> Install-Package NewtonSoft.JSON
3. Find the invoke method and note the _next
call. Some handlers can throw exceptions, so let's catch them. Wrap the _next
call with a try-catch
block and run our handler if an error is captured.
GanttErrorMiddleware.cs
public async Task Invoke(HttpContext httpContext)
{
try
{
await _next(httpContext);
}catch(Exception e)
{
await HandleExceptionAsync(httpContext, e);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var result = JsonConvert.SerializeObject(new {
action = "error"
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
return context.Response.WriteAsync(result);
}
4. Add the following namespaces to GanttErrorMiddleware.cs:
using Newtonsoft.Json;
5. The middleware is ready. Now go to Program.cs and connect the middleware. Add the following namespaces:
Program.cs
using DHX.Gantt;
Next call the app.UseGanttErrorMiddleware() method:
Program.cs
app.UseGanttErrorMiddleware();
Users can rearrange tasks with drag and drop in the client-side gantt. If you use this feature, you should store the order of tasks in the database. For details read this section.
Read on to find out how to enable storing the order of tasks for gantt.
First enable reordering of tasks on the client side. Add these lines to index.html:
wwwroot/index.html
gantt.config.order_branch = true;
gantt.config.order_branch_free = true;
// specifying the date format
gantt.config.date_format = "%Y-%m-%d %H:%i";
// initializing gantt
gantt.init("gantt_here");
Next you must change the backend so that it reflected the current order of tasks. Add one more method to the Task model:
Models/Task.cs
namespace DHX.Gantt.Models
{
public class Task
{
public int Id { get; set; }
public string? Text { get; set; }
public DateTime StartDate { get; set; }
public int Duration { get; set; }
public decimal Progress { get; set; }
public int? ParentId { get; set; }
public string? Type { get; set; }
public int SortOrder { get; set; } }
}
You will also need to update controllers.
1. The client side should receive tasks ordered by the SortOrder value. Add the highlighted line to DataController:
Controllers/DataController.cs
[HttpGet]
public object Get()
{
return new
{
data = _context.Tasks
.OrderBy(t => t.SortOrder) .ToList()
.Select(t => (WebApiTask)t),
links = _context.Links
.ToList()
.Select(l => (WebApiLink)l)
};
}
2. New tasks should also receive the default value SortOrder:
controllers/TaskController.cs
// POST api/task
[HttpPost]
public IActionResult Post(WebApiTask apiTask)
{
var newTask = (Models.Task)apiTask;
newTask.SortOrder = _context.Tasks.Max(t => t.SortOrder) + 1; _context.Tasks.Add(newTask);
_context.SaveChanges();
return Ok(new
{
tid = newTask.Id,
action = "inserted"
});
}
3. sortOrder should be updated when the task order is modified on the client. When a user rearranges tasks, gantt will call a PUT action and provide the info about the positions of the new task in the 'target' property of the request, together with the rest of task properties.
Add target
to the WebApiTask.cs class:
Models/WebApiTask.cs
public class WebApiTask
{
public int id { get; set; }
public string? text { get; set; }
public string? start_date { get; set; }
public int duration { get; set; }
public decimal progress { get; set; }
public int? parent { get; set; }
public string? type { get; set; }
public string? target { get; set; } public bool open
{
get { return true; }
set { }
}
}
And now let's implement reordering in our PUT (EditTask) action. Modify the Put action of the Task controller:
Controllers/TaskController.cs
// PUT api/task/5
[HttpPut("{id}")]
public IActionResult? Put(int id, WebApiTask apiTask)
{
var updatedTask = (Models.Task)apiTask;
updatedTask.Id = id;
var dbTask = _context.Tasks.Find(id);
if (dbTask == null)
{
return null;
}
dbTask.Text = updatedTask.Text;
dbTask.StartDate = updatedTask.StartDate;
dbTask.Duration = updatedTask.Duration;
dbTask.ParentId = updatedTask.ParentId;
dbTask.Progress = updatedTask.Progress;
dbTask.Type = updatedTask.Type;
if (!string.IsNullOrEmpty(apiTask.target)) { // reordering occurred this._UpdateOrders(dbTask, apiTask.target); }
_context.SaveChanges();
return Ok(new
{
action = "updated"
});
}
And add the method that will update the order of tasks:
Controllers/TaskController.cs
private void _UpdateOrders(Models.Task updatedTask, string orderTarget)
{
int adjacentTaskId;
var nextSibling = false;
var targetId = orderTarget;
// adjacent task id is sent either as '{id}' or as 'next:{id}' depending
// on whether it's the next or the previous sibling
if (targetId.StartsWith("next:"))
{
targetId = targetId.Replace("next:", "");
nextSibling = true;
}
if (!int.TryParse(targetId, out adjacentTaskId))
{
return;
}
var adjacentTask = _context.Tasks.Find(adjacentTaskId);
var startOrder = adjacentTask!.SortOrder;
if (nextSibling)
startOrder++;
updatedTask.SortOrder = startOrder;
var updateOrders = _context.Tasks
.Where(t => t.Id != updatedTask.Id)
.Where(t => t.SortOrder >= startOrder)
.OrderBy(t => t.SortOrder);
var taskList = updateOrders.ToList();
taskList.ForEach(t => t.SortOrder++);
}
Gantt doesn't provide any means of preventing an application from various threats, such as SQL injections, XSS and CSRF attacks. The responsibility for keeping an application safe is on the developers who implement the backend. Read the details in the corresponding article.
A simple solution would be to encode the text properties of your data items when you send them to the client side.
For example, in the below code a built-in HtmlEncoder is used to escape HTML values in the text of tasks. That way your database will contain unmodified data, but the client side will receive safe values of task.text
.
Models/WebApiTask.cs
using System.Text.Encodings.Web;
public static explicit operator WebApiTask(Task task)
{
return new WebApiTask
{
id = task.Id,
text = HtmlEncoder.Default.Encode(task.Text != null ? task.Text : ""), start_date = task.StartDate.ToString("yyyy-MM-dd HH:mm"),
duration = task.Duration,
parent = task.ParentId,
type = task.Type,
progress = task.Progress
};
}
Another approach would be to use a specialized library, e.g. HtmlAgilityPack and completely strip any HTML task when you save/load the data.
In case you've completed the above steps to implement Gantt integration with ASP.NET Core, but Gantt doesn't render tasks and links on a page, have a look at the Troubleshooting Backend Integration Issues article. It describes the ways of identifying the roots of the problems.
Now you have a fully functioning gantt. You can view the full code on GitHub, clone or download it and use it for your projects.
You can also check guides on the numerous features of gantt or tutorials on integrating Gantt with other backend frameworks.
Back to top