Это руководство объясняет, как создать приложение с диаграммой Ганта, используя ASP.NET и REST API на серверной стороне. Шаги включают настройку веб-платформы ASP.NET MVC 5 и использование контроллера Web API 2 для REST API. Entity Framework будет обрабатывать связь с базой данных, а для создания приложения будет использоваться Visual Studio.
Если вас интересуют другие интеграции на стороне сервера, ознакомьтесь с этими учебниками:
Полный исходный код для этого руководства доступен на GitHub.
Запустите Visual Studio 2022 и выберите опцию для создания нового проекта.
Выберите "ASP.NET Web Application" в качестве шаблона и назовите проект DHX.Gantt.Web. Если этот шаблон не виден, обратитесь к разделу Устранение неполадок.
Выберите шаблон 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();
}
}
}
Теперь создайте страницу index. Перейдите в 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) {
// указание формата даты
gantt.config.date_format = "%Y-%m-%d %H:%i";
// инициализация gantt
gantt.init("gantt_here");
// инициация загрузки данных
gantt.load("/api/data");
// инициализация dataProcessor
var dp = new gantt.dataProcessor("/api/");
// и привязка его к gantt
dp.init(gantt);
// установка режима REST для 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";
Диаграмма Ганта также настроена для работы с RESTful API, используя "/api/" в качестве маршрута по умолчанию:
Views/Home/Index.cshtml
gantt.load("/api/data");
// инициализация dataProcessor
var dp = new gantt.dataProcessor("/api/");
// и привязка его к gantt
dp.init(gantt);
// установка режима REST для dataProcessor
dp.setTransactionMode("REST");
На этом этапе вы можете запустить приложение, чтобы увидеть диаграмму Ганта, отображаемую на странице.
Диаграмма Ганта требует модели данных, состоящей из Tasks и Links. Клиентская модель следует определенной конвенции именования, которая отличается от стандартных C# конвенций. Некоторые свойства в клиентской модели могут не требовать хранения в базе данных, но полезны для клиента или логики на сервере.
Для обработки этого используется паттерн Data Transfer Object. Будут созданы отдельные классы доменной модели для использования с 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
Создайте класс GanttContext
, чтобы представить сессию с базой данных. Этот контекст позволяет извлекать и сохранять данные.
Добавьте новый класс в папку Models
с именем "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
. Назовите его "GanttInitializer" и наследуйте от класса DropCreateDatabaseIfModelChanges
. Переопределите метод Seed()
, чтобы заполнить базу данных тестовыми данными.
Вот полный код для класса 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)
{
// Код, который выполняется при запуске приложения
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
RouteConfig.RegisterRoutes(RouteTable.Routes);
Database.SetInitializer(new GanttInitializer());
}
}
}
Для начала вам нужно настроить классы DTO для Web API. Для отображения между моделью и 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, вам понадобятся три контроллера: один для задач, один для связей и другой для действия 'load data', чтобы обработать смешанный результат.
Чтобы создать новый контроллер:
Вот реализация основных действий 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()
};
}
}
}
Как только все будет на месте, запуск приложения должен отобразить полностью функциональную диаграмму Ганта:
Проверьте демонстрацию на GitHub.
Чтобы обрабатывать исключения в действиях CRUD и возвращать клиенту читаемые ответы, можно использовать фильтры исключений.
Вот как это можно настроить:
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
}
);
}
}
}
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
также нуждается в обновлениях:
SortOrder
: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);
}
SortOrder
по умолчанию: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"
});
}
SortOrder
должен обновляться при изменении порядка задач на клиенте.Когда задачи переставляются, Gantt отправляет запрос PUT с новым положением в свойстве ['target'](Интеграция на стороне сервера)
, вместе с другими деталями задачи. Чтобы обработать это, в класс TaskDto
добавляется дополнительное свойство:
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; }
...
}
}
Наконец, реализуйте логику изменения порядка в действии 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}', либо как '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++);
}
Запросы HTTP PUT и DELETE могут возвращать ошибки 405 или 401 при запуске приложения на IIS. Это может произойти из-за конфликта с модулем WebDAV. Чтобы решить эту проблему, отключите модуль в файле web.config. Подробнее об этом можно узнать здесь.
Gantt не включает встроенную защиту от угроз, таких как SQL-инъекции, XSS или CSRF-атаки. Обеспечение безопасности приложения возлагается на разработчиков, работающих над серверной частью. Более подробную информацию можно найти в этой статье.
Если шаблон проекта "ASP.NET Web Application" недоступен в Visual Studio 2022, выполните следующие шаги:
Найдите Visual Studio Community 2022 и нажмите Modify.
В установщике перейдите к Individual components, отметьте опцию ".NET Framework Project and item templates" и нажмите Modify.
После этого снова откройте Visual Studio 2022, и шаблон должен быть доступен.
Если возникает проблема с инициализатором DropCreateDatabaseIfModelChanges
, который не создает новую базу данных, обновите GanttInitializer.cs, чтобы использовать 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> {
...
}
}
Затем перезапустите приложение.
Если задачи и связи не отображаются на странице после выполнения шагов, ознакомьтесь с руководством по устранению неполадок в статье Устранение проблем с интеграцией бэкенда для получения советов по выявлению проблемы.
Ваш грид теперь полностью функционален. Полный код доступен на GitHub, где вы можете клонировать или скачать его для своих проектов.
Для получения дополнительных функций и руководств ознакомьтесь с документацией по функциям или учебниками по интеграции Gantt с другими серверными фреймворками.
К началу