本教程将为您提供一个清晰、循序渐进的指南,介绍如何使用 ASP.NET 和服务器端 REST API 创建甘特图。
如果您需要其他服务器端集成方案,可以参考以下教程:
本示例使用 ASP.NET MVC 5 框架及 Web API 2 控制器来构建甘特图应用的 REST API。Entity Framework 用于管理数据库交互。开发工作将在 Visual Studio IDE 中进行。
完整源代码已发布在 GitHub。
启动 Visual Studio 2022,选择 Create a new project。
然后选择 "ASP.NET Web Application",并命名为 DHX.Gantt.Web。如果未找到该模板,请参考 Troubleshooting 部分。
选择 Empty 项目模板,并确保勾选 MVC 和 Web API 选项。
项目创建完成后,下一步是添加一个 MVC 控制器,用于显示甘特图页面。
右键点击 Controllers 文件夹,选择 Add->Controller,然后选择 MVC 5 Controller -> Empty。将新控制器命名为 "HomeController"。
HomeController 默认包含 Index() 方法(继承自 ActionResult 类),因此无需添加额外逻辑。接下来将为该方法添加视图。
Controllers/HomeController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace DHX.Gantt.Web.Controllers
{
public class HomeController : Controller
{
// GET: Home
public ActionResult Index()
{
return View();
}
}
}
接下来创建首页。进入 Views/Home 文件夹,添加一个名为 Index 的空视图:
打开该视图,插入以下代码:
Views/Home/Index.cshtml
@{
Layout = null;
}
<!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>
上述代码实现了以下功能:
需要注意的是日期格式的配置:
Views/Home/Index.cshtml
gantt.config.date_format = "%Y-%m-%d %H:%i";
这样可以确保客户端能够正确解析从服务器接收到的日期。
此外,Gantt 配置为与后端 RESTful API 协同工作,使用 "/api/" 作为基础路由:
Views/Home/Index.cshtml
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");
服务器端的实现将在后续介绍。此时,您可以运行应用并看到甘特图出现在页面上。
接下来需要为甘特图定义模型类。数据模型由 Links 和 Tasks 组成。
dhtmlxGantt 的数据模型命名规范与常规 C# 命名有所不同。有些客户端属性无需存储在数据库中,但会在客户端或后端逻辑中使用。
为此,将采用 数据传输对象(DTO) 模式:领域模型类用于 EF 和内部逻辑,DTO 类用于与 Web API 通信。模型之间将实现映射。
让我们开始吧!
首先为 Task 创建一个类,示例如下:
Models/Task.cs
using System;
namespace DHX.Gantt.Web.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 类,如下所示:
Models/Link.cs
namespace DHX.Gantt.Web.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 管理。
在包管理器控制台中运行以下命令进行安装:
Install-Package EntityFramework
接下来创建数据库上下文。上下文代表与数据库的会话,并负责数据的检索和保存。
右键点击 Models 文件夹,选择 Add->Class,命名为 "GanttContext",内容如下:
Models/GanttContext.cs
using System.Data.Entity;
namespace DHX.Gantt.Web.Models
{
public class GanttContext : DbContext
{
public DbSet<Task> Tasks { get; set; }
public DbSet<Link> Links { get; set; }
}
}
现在可以向数据库添加一些初始数据。
Entity Framework 可以在应用首次运行时自动创建数据库。为了确保模型变更时数据库能自动更新,需要配置其在模型更改时删除并重建数据库。
首先创建数据库初始化器。在 App_Start 文件夹下添加一个继承自 DropCreateDatabaseIfModelChanges 的新类,命名为 "GanttInitializer"。
在该类中重写 Seed() 方法以插入测试数据。使用 Add() 方法将实体添加到上下文中。
以下为完整的 GanttInitializer 类:
App_Start/GanttInitializer.cs
using System;
using System.Collections.Generic;
using System.Data.Entity;
namespace DHX.Gantt.Web.Models
{
public class GanttInitializer : DropCreateDatabaseIfModelChanges<GanttContext>
{
protected override void Seed(GanttContext context)
{
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.SaveChanges();
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.SaveChanges();
}
}
}
打开 Global.asax 文件,该文件包含应用启动时执行的代码。添加所需命名空间,并在 Application_Start() 方法中插入以下代码,为上下文设置初始化器:
Global.asax.cs
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Http;
using System.Data.Entity;
using DHX.Gantt.Web.Models;
namespace DHX.Gantt.Web
{
public class Global : HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
// Code that runs on application startup
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
RouteConfig.RegisterRoutes(RouteTable.Routes);
Database.SetInitializer(new GanttInitializer()); }
}
}
接下来,将声明用于 Web API 的 DTO 类。为简化操作,将为 Model 和 DTO 类之间的映射定义显式转换操作符。
TaskDto 类的结构如下:
Models/TaskDto.cs
using System;
namespace DHX.Gantt.Web.Models
{
public class TaskDto
{
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 TaskDto(Task task)
{
return new TaskDto
{
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(TaskDto task)
{
return new Task
{
Id = task.id,
Text = task.text,
StartDate = DateTime.Parse(
task.start_date,
System.Globalization.CultureInfo.InvariantCulture),
Duration = task.duration,
ParentId = task.parent,
Type = task.type,
Progress = task.progress
};
}
}
}
类似地,LinkDto 类的定义如下:
Models/LinkDto.cs
namespace DHX.Gantt.Web.Models
{
public class LinkDto
{
public int id { get; set; }
public string type { get; set; }
public int source { get; set; }
public int target { get; set; }
public static explicit operator LinkDto(Link link)
{
return new LinkDto
{
id = link.Id,
type = link.Type,
source = link.SourceTaskId,
target = link.TargetTaskId
};
}
public static explicit operator Link(LinkDto link)
{
return new Link
{
Id = link.id,
Type = link.type,
SourceTaskId = link.source,
TargetTaskId = link.target
};
}
}
}
为了完成 数据源 的数据模型,添加如下类:
Models/GanttDto.cs
using System.Collections.Generic;
namespace DHX.Gantt.Web.Models
{
public class GanttDto
{
public IEnumerable<TaskDto> data { get; set; }
public IEnumerable<LinkDto> links { get; set; }
}
}
下一步是实现 API。
根据 API 详情,需要两个控制器:一个用于任务,一个用于链接。此外,还需要一个单独的控制器来处理“加载数据”操作,因为在这种情况下 gantt 期望 混合结果。
添加新控制器的方法:
任务条目的基本 CRUD 操作实现如下:
Controllers/TaskController.cs
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web.Http;
using System.Web.UI.WebControls;
using DHX.Gantt.Web.Models;
namespace DHX.Gantt.Web.Controllers
{
public class TaskController : ApiController
{
private GanttContext db = new GanttContext();
// GET api/Task
public IEnumerable<TaskDto> Get()
{
return db.Tasks
.ToList()
.Select(t => (TaskDto)t);
}
// GET api/Task/5
[System.Web.Http.HttpGet]
public TaskDto Get(int id)
{
return (TaskDto)db
.Tasks
.Find(id);
}
// PUT api/Task/5
[System.Web.Http.HttpPut]
public IHttpActionResult EditTask(int id, TaskDto taskDto)
{
var updatedTask = (Task)taskDto;
updatedTask.Id = id;
db.Entry(updatedTask).State = EntityState.Modified;
db.SaveChanges();
return Ok(new
{
action = "updated"
});
}
// POST api/Task
[System.Web.Http.HttpPost]
public IHttpActionResult CreateTask(TaskDto taskDto)
{
var newTask = (Task)taskDto;
db.Tasks.Add(newTask);
db.SaveChanges();
return Ok(new
{
tid = newTask.Id,
action = "inserted"
});
}
// DELETE api/Task/5
[System.Web.Http.HttpDelete]
public IHttpActionResult DeleteTask(int id)
{
var task = db.Tasks.Find(id);
if (task != null)
{
db.Tasks.Remove(task);
db.SaveChanges();
}
return Ok(new
{
action = "deleted"
});
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
}
}
其逻辑如下:
对于链接,也采用相同的方法。
为链接创建一个空的 Web API 控制器,如下所示:
Controllers/LinkController.cs
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web.Http;
using DHX.Gantt.Web.Models;
namespace DHX.Gantt.Web.Controllers
{
public class LinkController : ApiController
{
private GanttContext db = new GanttContext();
// GET api/Link
[System.Web.Http.HttpGet]
public IEnumerable<LinkDto> Get()
{
return db
.Links
.ToList()
.Select(l => (LinkDto)l);
}
// GET api/Link/5
[System.Web.Http.HttpGet]
public LinkDto Get(int id)
{
return (LinkDto)db
.Links
.Find(id);
}
// POST api/Link
[System.Web.Http.HttpPost]
public IHttpActionResult CreateLink(LinkDto linkDto)
{
var newLink = (Link)linkDto;
db.Links.Add(newLink);
db.SaveChanges();
return Ok(new
{
tid = newLink.Id,
action = "inserted"
});
}
// PUT api/Link/5
[System.Web.Http.HttpPut]
public IHttpActionResult EditLink(int id, LinkDto linkDto)
{
var clientLink = (Link)linkDto;
clientLink.Id = id;
db.Entry(clientLink).State = EntityState.Modified;
db.SaveChanges();
return Ok(new
{
action = "updated"
});
}
// DELETE api/Link/5
[System.Web.Http.HttpDelete]
public IHttpActionResult DeleteLink(int id)
{
var link = db.Links.Find(id);
if (link != null)
{
db.Links.Remove(link);
db.SaveChanges();
}
return Ok(new
{
action = "deleted"
});
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
}
}
最后,添加用于数据操作的控制器:
Controllers/DataController.cs
using System.Web.Http;
using DHX.Gantt.Web.Models;
namespace DHX.Gantt.Web.Controllers
{
public class DataController : ApiController
{
// GET api/
[System.Web.Http.HttpGet]
public GanttDto Get()
{
return new GanttDto
{
data = new TaskController().Get(),
links = new LinkController().Get()
};
}
}
}
完成上述步骤后,运行应用程序将在页面上显示一个完整可用的甘特图:
异常过滤器 提供了一种在 CRUD 处理程序中捕获异常并发送响应的方法,客户端 gantt 可以解析这些响应。
要在 gantt API 中启用错误处理,请按如下操作:
在 App_Start 文件夹下添加新类 GanttAPIExceptionFilterAttribute:
App_Start/GanttAPIExceptionFilterAttribute.cs
using System.Net;
using System.Net.Http;
using System.Web.Http.Filters;
namespace DHX.Gantt.Web
{
public class GanttAPIExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
context.Response = context.Request.CreateResponse(
HttpStatusCode.InternalServerError, new
{
action = "error",
message = context.Exception.Message
}
);
}
}
}
然后,将此过滤器应用于 WebAPI 控制器:
Controllers/DataController.cs
namespace DHX.Gantt.Web.Controllers
{
[GanttAPIExceptionFilter] public class DataController : ApiController
Controllers/LinkController.cs
namespace DHX.Gantt.Web.Controllers
{
[GanttAPIExceptionFilter] public class LinkController : ApiController
Controllers/TaskController.cs
namespace DHX.Gantt.Web.Controllers
{
[GanttAPIExceptionFilter] public class TaskController : ApiController
这样配置后,Web API 处理过程中抛出的任何异常都会导致客户端收到错误状态和消息,可以根据需要进行处理或显示。
请注意,直接向客户端暴露异常消息在生产环境中可能并不合适。
客户端 gantt 支持通过拖放方式重新排序任务。使用此功能时,需要将任务顺序保存到数据库。更多细节可参考此处总述。
下一步将是将此功能集成到应用程序中。
首先,用户应能够直接在界面中对任务进行排序。
打开 Index 视图,并按如下方式调整 gantt 配置:
Views/Home/Index.cshtml
gantt.config.order_branch = true;gantt.config.order_branch_free = true;
// 指定日期格式
gantt.config.date_format = "%Y-%m-%d %H:%i";
// 初始化 gantt
gantt.init("gantt_here");
接下来,让我们更新后端以支持这些更改。
任务顺序将存储在名为 SortOrder 的属性中,因此需要相应地更新 Task 类:
Models/Task.cs
using System;
using System.ComponentModel.DataAnnotations;
namespace DHX.Gantt.Web.Models
{
public class Task
{
public int Id { 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? ParentId { get; set; }
public string Type { get; set; }
public int SortOrder { get; set; } }
}
TaskController 也需要进行一些更新:
Controllers/TaskController.cs
namespace DHX.Gantt.Web.Controllers
{
[GanttAPIExceptionFilter]
public class TaskController : ApiController
{
private GanttContext db = new GanttContext();
// GET api/Task
public IEnumerable<TaskDto> Get()
{
return db.Tasks
.OrderBy(t => t.SortOrder) .ToList()
.Select(t => (TaskDto)t);
}
Controllers/TaskController.cs
namespace DHX.Gantt.Web.Controllers
{
[System.Web.Http.HttpPost]
public IHttpActionResult CreateTask(TaskDto taskDto)
{
var newTask = (Task)taskDto;
newTask.SortOrder = db.Tasks.Max(t => t.SortOrder) + 1;
db.Tasks.Add(newTask);
db.SaveChanges();
return Ok(new
{
tid = newTask.Id,
action = "inserted"
});
}
当任务被重新排序时,gantt 会触发一个包含新位置的 PUT 请求,其中新位置包含在 'target' 属性中以及其他任务详情。
为此,需要在任务 DTO 类中添加一个新属性:
Models/TaskDto.cs
namespace DHX.Gantt.Web.Models
{
public class TaskDto
{
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 string target { get; set; }
...
}
}
现在,在 PUT (EditTask) 操作中实现重新排序逻辑:
Controllers/TaskController.cs
// PUT api/Task/5
[System.Web.Http.HttpPut]
public IHttpActionResult EditTask(int id, TaskDto taskDto)
{
var updatedTask = (Task)taskDto;
updatedTask.Id = id;
if (!string.IsNullOrEmpty(taskDto.target))
{
// 发生了重新排序
this._UpdateOrders(updatedTask, taskDto.target); }
db.Entry(updatedTask).State = EntityState.Modified;
db.SaveChanges();
return Ok(new
{
action = "updated"
});
}
private void _UpdateOrders(Task updatedTask, string orderTarget) {
int adjacentTaskId;
var nextSibling = false;
var targetId = orderTarget;
// 相邻任务 id 通过 '{id}' 或 'next:{id}' 发送,取决于是下一个还是上一个兄弟节点
if (targetId.StartsWith("next:"))
{
targetId = targetId.Replace("next:", "");
nextSibling = true;
}
if (!int.TryParse(targetId, out adjacentTaskId))
{
return;
}
var adjacentTask = db.Tasks.Find(adjacentTaskId);
var startOrder = adjacentTask.SortOrder;
if (nextSibling)
startOrder++;
updatedTask.SortOrder = startOrder;
var updateOrders = db.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++);
}
在 IIS 上运行应用时,HTTP PUT 和 DELETE 请求可能返回 405 或 401 错误。这可能是由于 WebDAV 模块与 RESTful 处理程序冲突导致的。
常见的解决方法是在 web.config 文件中禁用 WebDAV 模块。更多细节可参见 这里。
Gantt 本身不负责防护 SQL 注入、XSS 或 CSRF 等威胁。在实现后端时,确保应用安全是开发者的责任。更多信息可参见 相关文章。
如果在 Visual Studio 2022 中找不到 "ASP.NET Web Application" 项目模板,请尝试以下操作:
1. 关闭 Visual Studio 2022
2. 打开开始菜单并启动 Visual Studio Installer
3. 找到 Visual Studio Community 2022 并点击 Modify
4. 在弹窗中,进入 Individual components,勾选 ".NET Framework Project and item templates",然后点击 Modify
之后,重新打开 Visual Studio 2022,模板应该就可用了。
有时,DropCreateDatabaseIfModelChanges 初始化器可能会删除现有数据库,但未能创建新数据库。
如果出现此问题,请打开 GanttInitializer.cs,将 DropCreateDatabaseIfModelChanges 替换为 DropCreateDatabaseAlways:
App_Start/GanttInitializer.cs
using System;
using System.Collections.Generic;
using System.Data.Entity;
namespace DHX.Gantt.Web.Models
{
public class GanttInitializer : DropCreateDatabaseAlways<GanttContext> {
...
}
}
然后重新运行应用。
如果在将 Gantt 集成到 ASP.NET MVC 后任务和链接未能渲染,请参考 后端集成问题排查指南 文章。该文档提供了排查潜在原因的指导。
此时,gantt 已经完全可用。完整代码可在 GitHub 获取,可克隆或下载用于项目开发。
此外,还可以查阅 各种 gantt 功能指南 或 与其他后端框架集成的教程。
Back to top