65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 dhtmlxGantt 的 ASP.NET MVC 甘特图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (20投票s)

2014年7月9日

GPL3

7分钟阅读

viewsIcon

50947

本文介绍如何将 dhtmlxGantt 集成到 ASP.NET MVC 应用程序中

在本教程中,我想向您展示如何将 dhtmlxGantt 集成到 ASP.NET MVC 应用程序中。dhtmlxGantt 是一个开源 (GPL) 的 JavaScript 库,可以绘制精美的甘特图,借助它,最终用户可以轻松便捷地编辑任务。跟随本教程,您将了解如何:1)在网页上放置基于 Ajax 的甘特图,2)从 .NET 服务器端加载任务,3)在用户在浏览器中进行更改时更新数据库中的任务。

如果您已经使用过 ASP.NET MVC 和 dhtmlxGantt,并且只想直接查看代码,可以 下载最终的演示应用程序

注意:示例不包含预先下载的 NuGet 包。它们将在您在 Visual Studio 中生成项目时自动安装。如果未发生这种情况,您可以配置 Visual Studio 以 在生成时还原丢失的包,或者通过包管理器控制台使用以下命令手动安装它们

>> Update-Package -Reinstall 

如果您不熟悉在 ASP.NET MVC 中使用 dhtmlxGantt,请按照本教程中的说明进行操作。

入门

首先,您需要创建一个新的 MVC 项目。

为了进一步工作,您还需要 dhtmlxGantt EntityFramework 库。您可以使用 Nuget 包管理器安装它们。

当所有预备工作完成后,您可以继续进行第一步。

第一步 – 初始化

首先,添加 dhtmlxGantt 文件的链接。打开 _Layout.cshtml 文件并进行如下修改

<!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> 

然后创建带有 Index 操作的 HomeController。将 dhtmlxGantt 的 HTML 容器添加到 Index 视图中

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

Scripts 文件夹中创建一个 main.js 文件,用于添加初始化页面上甘特图的代码。以下数据应添加到创建的文件中

(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
})(); 

在此代码中,我们指定了以下内容

- 时间刻度配置 - 两行刻度,将显示天和周;
- 里程碑和 任务详细信息表单 部分的标签文本;
- 用于数据加载的日期格式(这对于正确解析服务器端数据是必需的)。

然后,我们初始化了甘特图。

如果没有数据,您将看到运行的应用程序如下所示

第二步 – 创建任务和链接模型

此步骤涉及创建用于存储任务和链接的模型。请注意,我们在此处使用 EF Code First,这使我们无需手动创建数据库表。现在,在 Model 文件夹中创建 TaskLink

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; }
    }  

请注意,任务和链接的类可以包含任意数量的自定义列,这些列可以在客户端访问。

之后,创建数据库上下文类

- 创建一个新文件夹 DAL(用于数据访问层);
- 在该文件夹中创建一个名为 GanttContext.cs 的新类文件;
- 用以下代码替换模板代码

public class GanttContext : DbContext
    {
        public GanttContext() : base("GanttContext") { }
        public DbSet<GanttTask> GanttTasks { get; set; }
        public DbSet<GanttLink> GanttLinks { get; set; }
    } 

在此类中,我们将模型与数据库关联。上下文类将使用名为“GanttContext”的连接字符串,因此您需要定义一个才能获取数据库。

第三步 – 加载数据

现在我们来处理数据加载。通过客户端 dhtmlxGantt 组件,我们使用简单的 JSON 结构,如 此处 所述。基本上,它是一个具有两个数组属性的对象,一个用于链接,一个用于任务。任务日期应序列化为 main.js 中定义的 gantt.config.xml_date 中指定的格式。我们需要做的是创建一个带有 TasksLinks 两个数组的 JSON 对象。

HomeController.cs 中添加 Data 操作,并在 GanttContext 变量中访问数据库

// 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 };
}

我们继续配置 dhtmlxGantt 以从这些操作中 加载 数据。应将此行添加到 main.js 文件中的网格初始化之后

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

现在 dhtmlxGantt 能够加载数据了。为了填充我们的数据库,我们将配置 Entity Framework 以使用测试数据初始化数据库。在 DAL 文件夹中,创建一个名为 GanttInitializer.cs 的新类文件,并用以下代码替换模板代码,这将在需要时创建数据库并加载测试数据到新数据库中

   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();
        }
    } 

Seed 方法接收数据库上下文对象作为输入参数,并且方法中的代码使用该对象将新实体添加到数据库。对于每种实体类型,代码会创建一个新实体集合,将其添加到相应的 DbSet 属性,然后将更改保存到数据库。

现在您需要 Entity Framework 来使用您的 initializer 类。为此,请将一个元素添加到应用程序 Web.config 文件中的 entityFramework 元素中,如下例所示

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

dhtmlxGantt 现在已准备好加载数据。运行应用程序时,它将如下所示

第四步 – 将更改保存到数据库

通过完成所有这些步骤,我们现在有一个从数据库加载任务的甘特图,但这还不是结束。接下来,我们将把客户端的更改保存到服务器。让我们为此在客户端使用 dataProcessor。这个库已集成到 dhtmlxGantt 中,并自动跟踪客户端所做的更改并将更新发送到服务器,因此我们需要配置服务器来处理这些请求。

我们需要添加以下代码来创建一个类,该类将解析并表示从客户端发送的数据操作。将其添加到 Model 文件夹中新创建的 GanttRequest 模型

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 类具有几个属性来存储来自客户端的操作信息,以及一个方法,该方法从请求值创建数据操作集合。

数据以以下格式从客户端发送

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

这对于使用 dhtmlxDataprocessor 的所有 DHTMLX 组件来说是通用的。

单个请求可能指定对多个数据项的操作。每个属性都有一个前缀,该前缀链接到相关数据项的 ID。所有发送的 ID 都存储在“ids”参数中,并用逗号分隔

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

默认情况下,插入/更新的请求只包含一个 ID,而删除的请求可能指定多个项目。尽管如此,在一般情况下,请求可以包含任意数量的各种操作。

所以现在我们将当前区域性更改为 InvariantCulture。这对于更可预测地解析请求参数是必需的——从客户端组件接收到的日期和数字格式不取决于服务器端区域性设置。下一步是将请求值解析为单个数据操作的集合,并将其存储在 request 变量中(GanttRequest 类已在 Models/GanttRequest.cs 中定义)。

以下属性定义了数据操作的类型

  • Mode - 指定数据实体,它可以是 linktask
  • Actions - 指定操作的类型,可以是 delete、update、insert

根据操作和模式,我们从请求值中填充 UpdatedTaskUpdatedLink 对象。

更改将被保存,具体取决于操作,为此,我们将在 HomeController.cs 中创建 Save 操作,两个私有方法:UpdateTasksUpdateLinks。此外,我们创建 GanttRespose 方法用于 XML 响应。

  [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");
        }

Save 操作中,我们将请求值解析为单个数据操作的集合。然后,对于每个数据操作,根据其模式,我们在链接或任务表上执行更新。

完成上述操作后,我们需要向客户端返回响应。GanttRespose 方法渲染 XML 响应,该响应将通知客户端操作的结果(成功或错误)。

UpdateGanttTaksUpdateLinks 方法非常简单。它们调用 Entity Framework 函数来更新/插入/删除新的任务或链接。

XML 响应格式也非常简单

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> 

我们快完成了。最后一步是在客户端激活 dataProcessor。我们将以下行添加到 main.js 文件的末尾

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

这些更新将使您能够在应用程序中创建/更新/删除甘特图上的任务和链接。

本教程的最终成果是,您已经构建了一个带有 ASP.NET MVC 后端的交互式甘特图。对于最终用户来说,现在使用直观的拖放界面来创建和管理任务非常简单。所有更改都会保存到服务器并在数据库中更新。现在您将了解如何使用 dhtmlxGantt API 来扩展应用程序的功能并根据您的需求进行自定义。

下载最终的演示应用程序,它向您展示了 dhtmlxGantt 与 ASP.NET MVC 的集成。

© . All rights reserved.