Вот руководство, которое поможет вам настроить диаграмму Ганта с использованием 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. Если вы используете профессиональную версию компонента, вам нужно будет добавить файлы вручную.
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) {
// Установить формат даты
gantt.config.date_format = "%Y-%m-%d %H:%i";
// Инициализировать диаграмму Ганта
gantt.init("gantt_here");
// Загрузить данные
gantt.load("/api/data");
// Настроить dataProcessor
var dp = new gantt.dataProcessor("/api/");
dp.init(gantt);
dp.setTransactionMode("REST");
});
</script>
</head>
<body>
<div id="gantt_here" style="width: 100%; height: 100vh;"></div>
</body>
</html>
Когда страница загружается, диаграмма Ганта инициализируется, и запускается загрузка данных. Также настраивается dataProcessor
для сохранения изменений, внесенных в диаграмму, на сервере. Настройка серверной части будет выполнена позже.
Далее обновите Program.cs для использования страницы index.html. Это включает настройку приложения для обслуживания статических файлов из папки wwwroot
. Для этого добавьте метод app.UseDefaultFiles()
. Подробнее можно узнать здесь.
Program.cs
var builder = WebApplication.CreateBuilder(args);
// Добавить службы в контейнер.
builder.Services.AddRazorPages();
var app = builder.Build();
// Настроить конвейер обработки HTTP-запросов.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Метод app.UseDefaultFiles()
позволяет обслуживать файлы по умолчанию, такие как:
В этом руководстве используется "index.html". Обратите внимание, что UseDefaultFiles()
только переписывает URL — он не обслуживает файлы. Чтобы обслуживать файлы, также необходимо UseStaticFiles()
.
На данном этапе запуск приложения должен отобразить пустую диаграмму Ганта. Метка "Invalid data" в правом верхнем углу появляется, потому что вызывается gantt.load()
, но серверная часть еще не готова для предоставления данных. После реализации серверной части диаграмма покажет задачи и связи.
После завершения базовой настройки вы можете перейти к реализации серверной части. Начните с создания классов моделей, а затем перейдите к контроллеру WebAPI.
Модель данных Ганта включает задачи и связи. dhtmlxGantt использует нестандартные имена свойств по сравнению с конвенциями .NET. Иногда дополнительные свойства используются для клиентской или серверной логики, но они не обязательно должны храниться в базе данных.
Для обработки этого используется паттерн Data Transfer Object (DTO). Будут созданы два типа моделей:
Также будет реализовано отображение между этими моделями.
Создайте папку Models в каталоге вашего проекта для хранения классов моделей и контекста EF.
Начните с создания класса для задач. Добавьте файл с именем Task.cs в папку Models.
Вот структура модели задачи:
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; }
}
}
Полный список свойств задачи доступен для справки.
Далее создайте класс для связей, добавив файл с именем Link.cs в папку Models.
Вот структура модели связи:
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:
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; // База данных уже заполнена
}
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;"
}
}
Контекст базы данных будет зарегистрирован с использованием dependency injection.
Добавьте эти пространства имен в 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);
// Добавить службы в контейнер.
builder.Services.AddRazorPages();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<GanttContext>(
options => options.UseSqlServer(connectionString));
builder.Services.AddControllers();
var app = builder.Build();
// Настроить конвейер обработки HTTP-запросов.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// Значение HSTS по умолчанию - 30 дней.
// Возможно, вы захотите изменить это для производственных сценариев,
// см. https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
Чтобы настроить и заполнить базу данных при запуске приложения, вы создадите класс для инициализации. В папке 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 и seed.
С этой настройкой завершенной, вы можете перейти к Gantt.
Далее вы определите классы DTO для Web API. Начните с класса 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
};
}
}
}
Теперь создайте класс DTO для связей в файле с именем WebApiLink.cs, также внутри папки Models:
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 контроллера: один для задач, другой для связей и третий для всего набора данных:
Необходим контроллер для управления задачами. Он будет обрабатывать основные CRUD операции для задач Ганта.
Вот как это работает:
WebAPITask
, что является форматом, используемым dhtmlxGantt. Эти объекты необходимо преобразовать в формат класса Task
, используемый EntityFramework. После преобразования изменения могут быть сохранены в DatabaseContext
.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"
});
}
}
}
Теперь требуется контроллер для управления связями:
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)
};
}
}
}
На этом все настройки завершены. Теперь вы можете запустить приложение и увидеть полностью функциональную диаграмму Ганта.
Полный исходный код доступен на GitHub.
Для обработки ошибок можно создать класс middleware, который будет перехватывать исключения времени выполнения и записывать ответы. Этот класс будет добавлен в конвейер запросов приложения. Вот как это настроить:
1. Начните с создания класса middleware с использованием шаблона в папке проекта.
2. Установите JSON-фреймворк для ASP.NET Core. Вы можете сделать это через менеджер пакетов NuGet:
Или используйте командную строку Package Manager:
PM> Install-Package NewtonSoft.JSON
3. В методе Invoke
класса middleware оберните вызов _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. Наконец, подключите middleware в Program.cs. Добавьте необходимое пространство имен:
Program.cs
using DHX.Gantt;
Затем включите middleware в конвейер, вызвав:
Program.cs
app.UseGanttErrorMiddleware();
Если пользователи изменяют порядок задач с помощью перетаскивания, новый порядок можно сохранить в базе данных. Подробнее об этом можно узнать в этой секции.
Чтобы включить изменение порядка на клиентской стороне, добавьте следующие строки в index.html:
wwwroot/index.html
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");
Чтобы отразить изменения порядка задач на серверной стороне, обновите модель 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; } }
}
Пришло время внести некоторые изменения в контроллеры.
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)
};
}
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"
});
}
PUT
с новыми позициями задач в свойстве target
, вместе с другими данными задачи. Чтобы обработать это, добавьте свойство target
в класс WebApiTask
:
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 { }
}
}
Далее, измените действие Put
в TaskController
для обработки изменения порядка:
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)) { // произошло изменение порядка 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;
// идентификатор соседней задачи отправляется либо как '{id}', либо как 'next:{id}' в зависимости от того, является ли она следующим или предыдущим соседом
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-атаки. Разработчикам следует самостоятельно защитить свои приложения. Подробнее об этом можно узнать в соответствующей статье.
Простой способ защититься от XSS — закодировать текстовые свойства ваших данных перед их отправкой клиенту. Вот пример использования встроенного HtmlEncoder
для экранирования HTML в тексте задач. Это гарантирует, что в базе данных хранятся неизмененные данные, но клиент получает безопасные значения.
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 все равно не отображает задачи и связи, ознакомьтесь с руководством по устранению неполадок в этой статье. В нем содержатся советы по диагностике и устранению проблем.
После выполнения этих шагов у вас теперь есть полностью функциональная диаграмма Ганта. Полный код доступен на GitHub для клонирования или загрузки и использования в ваших проектах.
Для получения дополнительных функций изучите руководства по Gantt или ознакомьтесь с учебными материалами по интеграции Gantt с другими серверными фреймворками.
К началу