Перейти к основному содержимому

dhtmlxGantt с ASP.NET Core

Этот учебник даёт пошаговые инструкции по созданию Gantt с ASP.NET Core на серверной стороне.

Вы также можете прочитать руководства по другим технологиям на серверной стороне:

Для организации взаимодействия с базой данных используется Entity Framework Core. Приложение создаётся с помощью Visual Studio 2022.

заметка

Полный исходный код доступен на GitHub: GitHub.

Шаг 1. Создание проекта

Запустите Visual Studio 2022 и создайте новый проект. Выберите: Create a new project.

dhtmlxGantt с ASP.NET Core создание проекта

Далее выберите "ASP.NET Core Web App" и назовите проект DHX.Gantt.

dhtmlxGantt с ASP.NET Core создание проекта

dhtmlxGantt с ASP.NET Core настройка проекта

dhtmlxGantt с ASP.NET Core настройка проекта

Так как вы создали проект, можно переходить к добавлению разметки и скриптов для Gantt.

Шаг 2. Добавление разметки Gantt и JS

Перейдите в wwwroot и создайте файл index.html.

dhtmlxGantt с ASP.NET Core 2 создание проекта

dhtmlxGantt с ASP.NET Core 2 создание проекта

В newly созданном файле сделайте простую страницу для диаграммы Gantt.

Обратите внимание, что файлы Gantt добавляются с помощью CDN в этом демо. Если у вас есть Professional-версия компонента, вам нужно добавить файлы Gantt в ваш проект вручную.

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" />
<link href="css/site.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// 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');
// creating and configuring dataProcessor
const dp = gantt.createDataProcessor({
url: '/api/',
mode: 'REST',
});
});
</script>
</head>
<body>
<div id="gantt_here" style="width: 100vw; height: 100vh"></div>
</body>
</html>

Когда страница загружена, помимо (инициализации диаграммы) data loading сразу вызывается и dataProcessor настраивается, поэтому все изменения диаграммы Gantt, сделанные пользователем, будут сохранены на бекенд. Бэкенд ещё не реализован, поэтому позже будет иметь смысл.

Далее перейдите к Program.cs и скажите приложению использовать страницу index.html. Чтобы сделать это, необходимо настроить приложение на выдачу статических файлов из папки wwwroot. Для этого нужно добавить метод app.UseDefaultFiles(). Подробнее можно узнать здесь.

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
  • index.htm
  • default.html
  • default.htm

Таким образом, можно выбрать любой из них, однако в этом руководстве используется "index.html". UseDefaultFiles() — это всего лишь переписчик URL, который фактически файл не обслуживает. Для этой цели нужно также добавить файл UseStaticFiles(). Чтобы Gantt занимал всё пространство тела документа, добавьте следующие стили в файл site.css, который находится в папке wwwroot/css:

DHX.Gantt/wwwroot/css/site.css
html {
font-size: 14px;
}

@media (min-width: 768px) {
html {
font-size: 16px;
}
}

body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}

После выполнения этого на странице при запуске приложения должен появиться пустой Gantt. Обратите внимание, что надпись "Invalid data" в правом верхнем углу появляется, потому что вызывается gantt.load(), так как пока нет подходящего бекенда для подачи данных. Когда будет реализован контроллер, Gantt сможет отображать задачи и связи.

dhtmlxGantt с ASP.NET Core 2 добавление Gantt

Теперь базовая часть сделана и пора реализовать backend. Начнем с реализации моделей и далее перейдём к WebAPI контроллеру.

Создание моделей и базы данных

Начнем с моделей данных. Модель данных для Gantt включает ссылки и задачи. dhtmlxGantt использует нестандартные имена свойств моделей с точки зрения мира .NET. Иногда клиентская модель также содержит свойства для клиента или логики бэкенда, но эти свойства не должны сохраняться в базе данных.

Для этого будет использоваться шаблон Data Transfer Object (DTO). Определятся два типа моделей:

  • доменные модели, которые будут использоваться с EF Core и внутри приложения
  • DTO-модели, которые будут использоваться для взаимодействия с Web API.

Затем следует реализовать отображение между двумя моделями.

Модели

Создайте новую папку под названием Models в папке проекта. Здесь будут реализованы классы моделей и контекст EF.

Модель Task

Сначала создайте класс для задач. Создайте файл в папке Models и назовите его Task.cs. Это можно сделать через контекстное меню папки Models и выбрать 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.

Добавьте ещё один файл и создайте класс для Links:

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

Entity Framework Core будет использоваться для управления коммуникацией приложения с базой данных. Давайте установим фреймворк:

  • найдите зависимости DHTMLX.Gantt в дереве проекта
  • вызовите контекстное меню и выберите Manage NuGet packages
  • откройте вкладку Browse и установите Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore, и Microsoft.EntityFrameworkCore.Design

dhtmlxGantt с ASP.NET Core EF core установка

Или используйте командную строку пакетного менеджера:

PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore
PM> Install-Package Microsoft.EntityFrameworkCore.Design

Entity Framework Core будет использоваться для управления взаимодействием приложения с базой данных.

Создать Entity Context

Далее нужно определить контекст сессии с базой данных и включить загрузку и сохранение данных. Для этого создайте Context:

  • добавьте файл GanttContext.cs в папку Models
  • определите класс GanttContext в созданном файле
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;"
}
}

Контекст базы данных будет зарегистрирован через 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);

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

Наконец, нужно инициализировать и заполнить базу данных при запуске приложения. Обычно используют миграции, но здесь они не применяются для простоты.

Начнём с создания класса, в котором будет выполняться инициализация. Создайте файл GanttInitializerExtension.cs в папке Models:

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 и отображение

Настало время определить DTO-классы, которые будут использоваться для Web API. Начнём с DTO-класса для Task. В папке 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-класс для Link, определённый в файле 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
};
}
}
}

Когда вы завершите этот шаг, структура папок будет выглядеть следующим образом:

Gantt ASP.NET Core 2 All models

Теперь можно запустить приложение, чтобы проверить, что всё на месте. Если ошибок времени выполнения не видно, значит всё в порядке.

Шаг 4. Реализация Web API

Теперь настало время реализации самого REST API.

Добавление контроллеров

Создайте папку Controllers и три пустых API-контроллера: один для Tasks, другой для Links и ещё один для всего набора данных:

Gantt ASP.NET Core 2 adding controllers

Контроллер Task

Создадим контроллер для Tasks. Он будет определять базовые CRUD-операции для задач Gantt.

Как это работает:

  • в GET-запросах задачи загружаются из базы данных и возвращаются как DTO объектов задач;
  • в PUT/POST запросах задачи приходят от клиента как WebApiTask-классы. Они представлены в этом формате в dhtmlxGantt. Поэтому их следует преобразовать к формату нашей модели данных для EntityFramework (класс Task). Затем можно сохранить изменения в DatabaseContext.
Controllers/TaskController.cs
using Microsoft.AspNetCore.Mvc;
using DHX.Gantt.Models;
using Microsoft.EntityFrameworkCore;

namespace DHX.Gantt.Controllers
{
[Produces("application/json")]
[Route("api/task")]
public class TaskController : ControllerBase
{
private readonly GanttContext _context;
public TaskController(GanttContext context)
{
_context = context;
}

// GET api/task
[HttpGet]
public async Task<IEnumerable<WebApiTask>> Get()
{
return await _context.Tasks
.Select(t => (WebApiTask)t)
.ToListAsync();
}

// GET api/task/5
[HttpGet("{id}")]
public async Task<ActionResult<Models.Task>> Get(int id)
{
var task = await _context.Tasks.FindAsync(id);

if (task == null)
return NotFound();

return Ok(task);
}

// POST api/task
[HttpPost]
public async Task<IActionResult> Post(WebApiTask apiTask)
{
var newTask = (Models.Task)apiTask;

await _context.Tasks.AddAsync(newTask);
await _context.SaveChangesAsync();

return Ok(new
{
tid = newTask.Id,
action = "inserted"
});
}

// PUT api/task/5
[HttpPut("{id}")]
public async Task<IActionResult?> Put(int id, WebApiTask apiTask)
{
var updatedTask = (Models.Task)apiTask;
var dbTask = await _context.Tasks.FindAsync(id);

if (dbTask == null)
{
return NotFound();
}

dbTask.Text = updatedTask.Text;
dbTask.StartDate = updatedTask.StartDate;
dbTask.Duration = updatedTask.Duration;
dbTask.ParentId = updatedTask.ParentId;
dbTask.Progress = updatedTask.Progress;
dbTask.Type = updatedTask.Type;

await _context.SaveChangesAsync();

return Ok(new
{
action = "updated"
});
}

// DELETE api/task/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTask(int id)
{
var task = await _context.Tasks.FindAsync(id);
if (task != null)
{
_context.Tasks.Remove(task);
await _context.SaveChangesAsync();
}

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 : ControllerBase
{
private readonly GanttContext _context;
public LinkController(GanttContext context)
{
_context = context;
}

// GET api/Link
[HttpGet]
public async Task<IEnumerable<WebApiLink>> Get()
{
return await _context.Links
.Select(t => (WebApiLink)t)
.ToListAsync();
}

// GET api/Link/5
[HttpGet("{id}")]
public async Task<ActionResult<Link>> Get(int id)
{
var link = await _context.Links.FindAsync(id);

if (link == null)
return NotFound();

return Ok(link);
}

// POST api/Link
[HttpPost]
public async Task<IActionResult> Post(WebApiLink apiLink)
{
var newLink = (Link)apiLink;

_context.Links.Add(newLink);
await _context.SaveChangesAsync();

return Ok(new
{
tid = newLink.Id,
action = "inserted"
});
}

// PUT api/Link/5
[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, WebApiLink apiLink)
{
var updatedLink = (Link)apiLink;
updatedLink.Id = id;
_context.Entry(updatedLink).State = EntityState.Modified;


await _context.SaveChangesAsync();

return Ok(new
{
action = "updated"
});
}

// DELETE api/Link/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteLink(int id)
{
var link = await _context.Links.FindAsync(id);
if (link != null)
{
_context.Links.Remove(link);
await _context.SaveChangesAsync();
}

return Ok(new
{
action = "deleted"
});
}
}
}

Контроллер Data

Наконец, нужно создать контроллер для действия данных:

Controllers/DataController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DHX.Gantt.Models;

namespace DHX.Gantt.Controllers
{
[Produces("application/json")]
[Route("api/data")]
public class DataController : ControllerBase
{
private readonly GanttContext _context;
public DataController(GanttContext context)
{
_context = context;
}

// GET api/data
[HttpGet]
public async Task<IActionResult> Get()
{
var tasks = await _context.Tasks
.Select(t => (WebApiTask)t)
.ToListAsync();

var links = await _context.Links
.Select(l => (WebApiLink)l)
.ToListAsync();

return Ok(new
{
tasks,
links
});
}
}
}

Всё готово. Можно запустить приложение и увидеть полностью функциональный Gantt.

Gantt ASP.NET Core Gantt is ready

Полный исходный код cũng можно просмотреть на GitHub.

Обработка ошибок

Чтобы обрабатывать ошибки, нужно объявить специальный класс middleware, который будет перехватывать исключения во время выполнения и записывать ответы. Затем его добавят в конвейер обработки запросов приложения. Выполните следующие шаги:

  1. Создайте middleware-класс на основе шаблона в папке проекта.

Gantt ASP.NET Core middleware class

  1. Установите JSON-фреймворк для ASP.NET Core. Это можно сделать через диспетчер пакетами NuGet:

Gantt ASP.NET Core Install NewtonSoft Json

Или воспользуйтесь командной строкой пакетного менеджера:

PM> Install-Package NewtonSoft.JSON
  1. Найдите метод invoke и обратите внимание на вызов _next. Некоторые обработчики могут выбросить исключение, поэтому давайте перехватим его. Оборачиваем вызов _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);
}
  1. Добавьте следующие пространства имён в GanttErrorMiddleware.cs:
using Newtonsoft.Json;
  1. middleware готов. Теперь перейдите к Program.cs и подключите middleware. Добавьте следующее пространство имён:
Program.cs
using DHX.Gantt;

Далее вызовите метод app.UseGanttErrorMiddleware():

Program.cs
app.UseGanttErrorMiddleware();

Хранение порядка задач

Пользователи могут перетаскивать задачи вправо/влево на клиенте. Если вы используете эту возможность, следует сохранять порядок задач в базе данных. См. подробнее в этом разделе.

Далее — как включить сохранение порядка задач для Gantt.

Перестановка на клиентской стороне

Сначала включите изменение порядка задач на клиентской стороне. Добавьте эти строки в 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 async Task<IActionResult> Get()
{
var tasks = await _context.Tasks
.OrderBy(t => t.SortOrder) /*!*/
.Select(t => (WebApiTask)t)
.ToListAsync();

var links = await _context.Links
.Select(l => (WebApiLink)l)
.ToListAsync();

return Ok(new
{
data = tasks,
links = links
});
}
  1. Новые задачи также должны получать значение по умолчанию SortOrder:
controllers/TaskController.cs
// POST api/task
[HttpPost]
public async Task<IActionResult> Post(WebApiTask apiTask)
{
var newTask = (Models.Task)apiTask;

newTask.SortOrder = await _context.Tasks.MaxAsync(t => t.SortOrder) + 1; /*!*/
await _context.Tasks.AddAsync(newTask);
await _context.SaveChangesAsync();

return Ok(new
{
tid = newTask.Id,
action = "inserted"
});
}
  1. Значение sortOrder должно обновляться, когда порядок задач на клиенте меняется. Когда пользователь переставляет задачи, Gantt вызывает PUT-действие и передаёт информацию об позициях новой задачи в свойстве target запроса, вместе с остальными свойствами задачи. Добавьте свойство target в класс WebApiTask.cs:
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- (EditTask) действии. Измените действие Put в TaskController:

Controllers/TaskController.cs
// PUT api/task/5
[HttpPut("{id}")]
public async Task<IActionResult?> Put(int id, WebApiTask apiTask)
{
var updatedTask = (Models.Task)apiTask;
var dbTask = await _context.Tasks.FindAsync(id);

if (dbTask == null)
{
return NotFound();
}

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)) /*!*/
{ /*!*/
// перестановка произошла /*!*/
await this.UpdateOrdersAsync(dbTask, apiTask.target); /*!*/
} /*!*/

await _context.SaveChangesAsync();

return Ok(new
{
action = "updated"
});
}

И добавим метод, который будет обновлять порядок задач:

Controllers/TaskController.cs
private async Task<IActionResult> UpdateOrdersAsync(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 NotFound();
}

var adjacentTask = await _context.Tasks.FindAsync(adjacentTaskId);
if (adjacentTask == null)
{
return NotFound();
}
var startOrder = adjacentTask.SortOrder;

if (nextSibling)
startOrder++;

updatedTask.SortOrder = startOrder;

var updateOrders = await _context.Tasks
.Where(t => t.Id != updatedTask.Id)
.Where(t => t.SortOrder >= startOrder)
.OrderBy(t => t.SortOrder)
.ToListAsync();

var taskList = updateOrders.ToList();
taskList.ForEach(t => t.SortOrder++);

return Ok(new
{
action = "updated"
});
}

Безопасность приложения

Gantt не предоставляет механизма защиты от разных угроз, таких как SQL-инъекции, XSS и CSRF-атаки. Ответственность за безопасность приложения лежит на разработчиках, которые реализуют бекенд. Подробности в соответствующей статье guid es/app-security.md.

Защита от XSS

Простое решение — кодировать текстовые свойства ваших элементов данных перед отправкой на клиентскую сторону.

Например, ниже приведён пример использования встроенного 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, но Gantt не рендерит задачи и связи на странице, посмотрите статью Troubleshooting Backend Integration Issues. Она описывает способы выявления корней проблем.

Что дальше

Теперь у вас полнофункциональный Gantt. Полный код можно посмотреть на GitHub, клонировать или загрузить и использовать в ваших проектах.

Также можно ознакомиться с guides на множество функций Gantt или руководствами по интеграции Gantt с другими бекенд-фреймворками.

Need help?
Got a question about the documentation? Reach out to our technical support team for help and guidance. For custom component solutions, visit the Services page.