本指南将带您逐步完成在服务端使用 ASP.NET Core 搭建甘特图的过程。
同时,我们还为其他服务端平台提供了教程:
数据库交互部分采用了 Entity Framework Core。项目开发工具为 Visual Studio 2022。
完整源代码可在 GitHub 查看。
启动 Visual Studio 2022,选择 Create a new project 新建项目。
选择 “ASP.NET Core Web App”,项目名称设置为 DHX.Gantt。
项目创建完成后,即可添加甘特图所需的标记和脚本。
进入 wwwroot 目录,新建名为 index.html 的文件。
在该文件中,构建一个简单页面用于展示甘特图。
请注意,本例中甘特图文件是从 CDN 加载的。如果您拥有 Professional 版本,需要 手动将甘特图文件添加到项目中。
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>
页面加载后,甘特图会被初始化,并通过 gantt.load()
立即开始加载数据。同时 dataProcessor
也被配置好,用户在图表上的修改会自动保存到服务器。由于后端尚未搭建,完整功能将在后续实现后体现。
接下来,打开 Program.cs,配置应用以便能够访问 index.html 页面。需要通过添加 app.UseDefaultFiles()
启用从 wwwroot
文件夹提供静态文件。
更多信息请参考 这里。
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();
app.UseDefaultFiles()
方法会查找 wwwroot 目录下的默认文件:
您可以使用上述任意文件名,本教程采用 "index.html"。
请注意,UseDefaultFiles()
仅重写 URL,并不会实际提供文件。要真正提供静态文件,还需添加 UseStaticFiles()
。
完成上述步骤后,运行应用将显示一个空的甘特图。右上角会出现 "Invalid data" 提示,因为 gantt.load()
被调用但后端尚未提供数据。待控制器实现后,甘特图会正确显示任务和链接。
基础环境搭建完毕,接下来构建后端。首先定义模型类,然后创建 WebAPI 控制器。
首先定义数据模型。典型的甘特图数据模型包括 links 和 tasks。 dhtmlxGantt 使用的属性名与 .NET 习惯不同,且部分属性仅用于客户端或后端逻辑,不应存入数据库。
为处理此问题,将采用 数据传输对象(DTO) 模式。将创建两类模型:
还需要实现这两类模型之间的映射。
在项目目录下新建 Models 文件夹。此文件夹用于存放模型类与 EF 上下文。
创建任务类。在 Models 文件夹下新建 Task.cs 文件(右键文件夹选择 Add->Class)。
类定义如下:
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; }
}
}
完整的 Task 对象属性列表参见 这里。
为 Link 新建文件:
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; }
}
}
模型准备就绪后,下一步配置数据库连接。
按照以下步骤配置数据库连接:
Entity Framework Core 用于数据库通信。安装方法如下:
也可使用 Package Manager Console:
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore
PM> Install-Package Microsoft.EntityFrameworkCore.Design
这些包将为数据库交互提供所需工具。
接下来,定义与数据库的会话以实现数据加载和保存。新建上下文类:
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;
}
}
现在可以为数据库添加一些初始记录。为此需创建数据库初始化器,实现插入任务和链接的功能。 在 Models 文件夹下定义 GanttSeeder 类。该类包含一个 Seed() 方法,用于向数据库添加任务和链接。
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();
}
}
}
}
接下来,需要在 Program.cs 文件中注册数据库。在此之前,需要准备一个连接字符串。
该连接字符串将会被存储在应用程序设置的 JSON 文件中。
创建 appsettings.json 文件(如果已经存在则直接打开),并添加数据库的连接字符串:
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;
Database=GanttDatabase;Trusted_Connection=True;"
}
}
数据库上下文将通过
依赖注入 的方式进行注册。
在 Program.cs 文件中添加以下命名空间:
Program.cs
using Microsoft.EntityFrameworkCore;
using DHX.Gantt.Models;
注册的代码如下所示:
Program.cs
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<GanttContext>(
options => options.UseSqlServer(connectionString));
为了启用控制器,需要添加 services.AddControllers() 方法:
Program.cs
builder.Services.AddControllers();
并通过调用 app.MapControllers() 注册控制器路由:
Program.cs
app.MapControllers();
以下是 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();
最后,当应用启动时应该初始化并填充数据库。虽然通常会使用迁移(migrations)来实现,但本例为简化起见未使用迁移。
首先,在 Models 文件夹下创建一个类用于初始化。添加 GanttInitializerExtension.cs 文件:
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;
}
}
}
然后像下面这样调用 InitializeDatabase() 方法:
Program.cs
app.InitializeDatabase();
如前所述,本教程未使用迁移,而是通过 EnsureCreated 和数据填充(seeding)来实现。
到此为止,本部分内容已经完成。接下来,将继续处理甘特图相关工作。
现在需要为 Web API 创建将要使用的 DTO 类。
首先为任务(Task)创建 DTO 类。在 Models 文件夹下新建文件并定义 WebApiTask.cs 类:
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
};
}
}
}
接下来是 Link 的 DTO 类,在 Models 文件夹下的 WebApiLink.cs 文件中定义:
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
};
}
}
}
完成上述步骤后,文件夹结构应如下所示:
此时建议运行应用程序以验证一切是否设置正确。如果没有出现运行时错误,说明设置成功。
现在是实现 REST API 的时候了。
创建一个 Controllers 文件夹,并添加三个空的 API 控制器:分别用于 Tasks、Links 以及整个数据集:
以下是用于管理任务(Tasks)的控制器,实现了甘特任务的基本 CRUD 操作。
功能说明:
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"
});
}
}
}
接下来是 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"
});
}
}
}
最后,这里是数据操作的控制器:
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)
};
}
}
}
至此,所有内容都已就绪,应用程序可以运行,你将看到一个完整可用的甘特图。
为了有效管理错误,你需要创建一个特殊的 middleware class 来捕获运行时异常并发送适当的响应。该中间件随后会被添加到应用的请求管道中。操作步骤如下:
1. 使用模板向你的项目添加一个中间件类。
2. 为 ASP.NET Core 安装 JSON 框架。可以通过 NuGet 包管理器完成:
或者使用包管理器控制台:
PM> Install-Package NewtonSoft.JSON
3. 找到中间件中的 Invoke 方法。由于有些处理程序可能会抛出异常,请将 _next
调用放入 try-catch
块中,在发生错误时处理异常。
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. 在 GanttErrorMiddleware.cs 文件中添加如下命名空间:
using Newtonsoft.Json;
5. 中间件已准备就绪。接下来,打开 Program.cs 并注册该中间件,添加如下内容:
Program.cs
using DHX.Gantt;
然后通过以下调用将中间件加入管道:
Program.cs
app.UseGanttErrorMiddleware();
当用户在客户端通过拖拽重新排序任务时,新顺序应保存到数据库。详细内容请参阅 本节。
下面介绍如何在甘特图中启用任务顺序存储。
首先,在客户端启用任务排序,在 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");
然后,更新后端以反映当前任务顺序。在 Task 模型中添加新属性:
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; } }
}
控制器也需要做一些更新。
1. 客户端应按 SortOrder 排序获取任务。在 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. 创建新任务时,确保它们获得默认的 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。当任务被重新排列时,gantt 会通过 PUT 请求发送包含新位置信息的 'target' 属性,以及其他任务详情。
在 WebApiTask.cs 类中添加 target
属性:
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 { }
}
}
现在,在 Task 控制器的 PUT (EditTask) 方法中实现重新排序逻辑:
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 happened this._UpdateOrders(dbTask, apiTask.target); }
_context.SaveChanges();
return Ok(new
{
action = "updated"
});
}
添加用于调整任务顺序的辅助方法:
Controllers/TaskController.cs
private void _UpdateOrders(Models.Task updatedTask, string orderTarget)
{
int adjacentTaskId;
var nextSibling = false;
var targetId = orderTarget;
// adjacent task id may come as '{id}' or 'next:{id}' indicating
// whether it's the next or 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 本身不负责防范如 SQL 注入、XSS 或 CSRF 等安全威胁。后端开发者需自行保证应用的安全。详情请参考 相关文档。
一种简单的方法是在将文本字段发送到客户端之前进行编码。
例如,下面的示例使用内置的 HtmlEncoder 对任务文本中的 HTML 进行转义。这样,数据库中保存的是原始数据,但客户端收到的是安全的 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
};
}
另外,也可以使用如 HtmlAgilityPack 这样的专业库,在保存或加载任务时彻底移除所有 HTML。
如果你已按照所有步骤将 Gantt 集成到 ASP.NET Core,但页面上未显示任务和链接,请参阅 后端集成问题排查指南 文章。文中涵盖了常见问题及其排查方法。
此时,你已经拥有了一个可用的甘特图实现。完整源码可在 GitHub 上获取,可用于你的项目。
你还可以查阅 涵盖众多甘特功能的指南 或 集成 Gantt 到其他后端框架的教程。
Back to top