Это руководство описывает процесс создания Scheduler с использованием ASP.NET Core на серверной стороне.
Вы также можете ознакомиться с руководствами для других серверных платформ:
Взаимодействие с базой данных осуществляется с помощью Entity Framework Core. Пример разработан с использованием Visual Studio 2022.
Полный исходный код доступен на GitHub.
Запустите Visual Studio 2022 и создайте новый проект, выбрав "Create a new project".
Затем выберите "ASP.NET Core Web App" и задайте имя проекта SchedulerApp
На этом этапе проект готов, и вы можете перейти к добавлению разметки и скриптов для Scheduler.
Перейдите в папку wwwroot и создайте новый файл с именем index.html.
В этом файле создайте простую страницу для отображения scheduler.
Обратите внимание, что в этом примере файлы scheduler загружаются с CDN. Если у вас есть Professional-версия, необходимо добавить файлы scheduler в проект вручную.
index.html
<!DOCTYPE html>
<html>
<head>
<title>Getting started with dhtmlxScheduler</title>
<meta charset="utf-8">
<script src="https://cdn.dhtmlx.com/scheduler/edge/dhtmlxscheduler.js"></script>
<link href="https://cdn.dhtmlx.com/scheduler/edge/dhtmlxscheduler.css"
rel="stylesheet" type="text/css" charset="utf-8">
<style>
html, body {
margin: 0px;
padding: 0px;
}
</style>
</head>
<body>
<div id="scheduler_here" class="dhx_cal_container" style='width:100%; height:100vh;'>
<div class="dhx_cal_navline">
<div class="dhx_cal_prev_button"> </div>
<div class="dhx_cal_next_button"> </div>
<div class="dhx_cal_today_button"></div>
<div class="dhx_cal_date"></div>
<div class="dhx_cal_tab" name="day_tab"></div>
<div class="dhx_cal_tab" name="week_tab"></div>
<div class="dhx_cal_tab" name="month_tab"></div>
</div>
<div class="dhx_cal_header"></div>
<div class="dhx_cal_data"></div>
</div>
<script>
scheduler.init('scheduler_here', new Date(2019, 0, 20), "week");
</script>
</body>
</html>
Далее откройте 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".
Обратите внимание, что UseDefaultFiles()
только переписывает URL на стандартный файл, поэтому также необходимо использовать UseStaticFiles()
для фактической отдачи файла.
После этих изменений при запуске приложения на странице отобразится пустой scheduler.
Дальнейшие шаги покажут, как создать backend API и подключить к нему scheduler.
Начнем с модели данных. Для событий scheduler требуется класс-представление. Поскольку dhtmlxScheduler использует нестандартные имена свойств по сравнению с типичными соглашениями .NET, будет применен паттерн Data Transfer Object (DTO). Это подразумевает определение:
Также будет реализовано сопоставление между этими моделями.
Добавьте в проект новую папку Models. Здесь будут храниться классы моделей и EF-контекст.
Создайте класс, представляющий события календаря. Добавьте файл SchedulerEvent.cs в папку Models.
Пример простой модели:
SchedulerApp/Models/SchedulerEvent.cs
namespace SchedulerApp.Models
{
public class SchedulerEvent
{
public int Id { get; set; }
public string? Name { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
}
Имейте в виду, что события scheduler могут содержать и другие свойства, но в этом примере рассмотрены только основные.
Сначала установите Entity Framework Core для ASP.NET Core. Это можно сделать через менеджер пакетов NuGet:
Или выполнив следующие команды в консоли диспетчера пакетов:
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore
PM> Install-Package Microsoft.EntityFrameworkCore.Design
Entity Framework Core будет отвечать за взаимодействие приложения с базой данных.
Далее определите сессию базы данных и настройте загрузку и сохранение данных, создав класс Context:
SchedulerApp/Models/SchedulerContext.cs
using Microsoft.EntityFrameworkCore;
namespace SchedulerApp.Models
{
public class SchedulerContext : DbContext
{
public SchedulerContext(DbContextOptions<SchedulerContext> options)
: base(options)
{
}
public DbSet<SchedulerEvent> Events { get; set; } = null!;
}
}
Теперь пора добавить первые записи в базу данных. Создайте инициализатор базы данных, который заполнит её примерами событий.
В папке Models добавьте класс SchedulerSeeder. В нем будет метод Seed(), который вставляет события в базу данных.
SchedulerApp/Models/SchedulerSeeder.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace SchedulerApp.Models
{
public static class SchedulerSeeder
{
public static void Seed(SchedulerContext context)
{
if (context.Events.Any())
{
return; // DB has been seeded
}
var events = new List<SchedulerEvent>()
{
new SchedulerEvent
{
Name = "Event 1",
StartDate = new DateTime(2019, 1, 15, 2, 0, 0),
EndDate = new DateTime(2019, 1, 15, 4, 0, 0)
},
new SchedulerEvent()
{
Name = "Event 2",
StartDate = new DateTime(2019, 1, 17, 3, 0, 0),
EndDate = new DateTime(2019, 1, 17, 6, 0, 0)
},
new SchedulerEvent()
{
Name = "Multiday event",
StartDate = new DateTime(2019, 1, 15, 0, 0, 0),
EndDate = new DateTime(2019, 1, 20, 0, 0, 0)
}
};
events.ForEach(s => context.Events.Add(s));
context.SaveChanges();
}
}
}
Следующим шагом является регистрация базы данных в Program.cs. Перед этим потребуется строка подключения. Эта строка будет храниться
в JSON-файле в настройках приложения.
Если проект был создан с помощью шаблона API
, этот файл уже должен присутствовать в папке проекта. Для тех, кто использовал Empty template
, файл необходимо создать вручную.
Создайте файл appsettings.json (или откройте его, если он уже существует) и добавьте строку подключения к базе данных:
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection":"Server=(localdb)\\mssqllocaldb;
Database=SchedulerDatabase;Trusted_Connection=True;" }
}
После этого зарегистрируйте контекст базы данных через dependency injection.
Добавьте следующие пространства имён в Program.cs:
Program.cs
using Microsoft.EntityFrameworkCore;
using SchedulerApp.Models;
using Microsoft.Extensions.Configuration;
Регистрация будет выглядеть так:
Program.cs
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<SchedulerContext>(
options => options.UseSqlServer(connectionString));
Для активации контроллеров вызовите метод services.AddControllers()
.
Program.cs
builder.Services.AddControllers();
Ниже приведено полное содержимое файла Program.cs:
Program.cs
using Microsoft.EntityFrameworkCore;
using SchedulerApp.Models;
using Microsoft.Extensions.Configuration;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<SchedulerContext>(
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();
Наконец, база данных должна быть инициализирована и заполнена начальными данными при запуске приложения. Обычно для этого используются миграции, но здесь они опущены для простоты.
Начните с создания класса для инициализации. Добавьте файл SchedulerInitializerExtension.cs в папку Models:
Models/SchedulerInitializerExtension.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
namespace SchedulerApp.Models
{
public static class SchedulerInitializerExtension
{
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<SchedulerContext>();
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
SchedulerSeeder.Seed(dbContext);
}
return webHost;
}
}
}
Затем вызовите InitializeDatabase():
Program.cs
app.InitializeDatabase();
На этом этапе эта часть завершена; далее внимание переключается на Scheduler.
Пора определить классы DTO для Web API. Начните с класса DTO для SchedulerEvent. В папке Models создайте файл и определите класс WebAPIEvent.cs:
Models/WebApiEvent.cs
using System;
namespace SchedulerApp.Models
{
public class WebAPIEvent
{
public int id { get; set; }
public string? text { get; set; }
public string? start_date { get; set; }
public string? end_date { get; set; }
public static explicit operator WebAPIEvent(SchedulerEvent ev)
{
return new WebAPIEvent
{
id = ev.Id,
text = ev.Name,
start_date = ev.StartDate.ToString("yyyy-MM-dd HH:mm"),
end_date = ev.EndDate.ToString("yyyy-MM-dd HH:mm")
};
}
public static explicit operator SchedulerEvent(WebAPIEvent ev)
{
return new SchedulerEvent
{
Id = ev.id,
Name = ev.text,
StartDate = ev.start_date != null ? DateTime.Parse(ev.start_date,
System.Globalization.CultureInfo.InvariantCulture) : new DateTime(),
EndDate = ev.end_date != null ? DateTime.Parse(ev.end_date,
System.Globalization.CultureInfo.InvariantCulture) : new DateTime()
};
}
}
}
После этого структура папки будет выглядеть так:
На этом этапе можно запустить приложение и убедиться, что всё настроено правильно. Если во время выполнения не возникает ошибок, значит настройка прошла успешно.
Далее реализуется REST API.
Создайте папку Controllers и добавьте в неё пустой API-контроллер для событий:
Controllers/EventsController.cs
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using SchedulerApp.Models;
namespace SchedulerApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EventsController : ControllerBase
{
private readonly SchedulerContext _context;
public EventsController(SchedulerContext context)
{
_context = context;
}
// GET api/events
[HttpGet]
public IEnumerable<WebAPIEvent> Get()
{
return _context.Events
.ToList()
.Select(e => (WebAPIEvent)e);
}
// GET api/events/5
[HttpGet("{id}")]
public SchedulerEvent? Get(int id)
{
return _context
.Events
.Find(id);
}
// POST api/events
[HttpPost]
public ObjectResult Post([FromForm] WebAPIEvent apiEvent)
{
var newEvent = (SchedulerEvent)apiEvent;
_context.Events.Add(newEvent);
_context.SaveChanges();
return Ok(new
{
tid = newEvent.Id,
action = "inserted"
});
}
// PUT api/events/5
[HttpPut("{id}")]
public ObjectResult? Put(int id, [FromForm] WebAPIEvent apiEvent)
{
var updatedEvent = (SchedulerEvent)apiEvent;
var dbEvent = _context.Events.Find(id);
if (dbEvent == null)
{
return null;
}
dbEvent.Name = updatedEvent.Name;
dbEvent.StartDate = updatedEvent.StartDate;
dbEvent.EndDate = updatedEvent.EndDate;
_context.SaveChanges();
return Ok(new
{
action = "updated"
});
}
// DELETE api/events/5
[HttpDelete("{id}")]
public ObjectResult DeleteEvent(int id)
{
var e = _context.Events.Find(id);
if (e != null)
{
_context.Events.Remove(e);
_context.SaveChanges();
}
return Ok(new
{
action = "deleted"
});
}
}
}
Когда Web API готов, вернитесь к HTML-странице и настройте scheduler для работы с ним:
wwwroot/index.html
scheduler.config.date_format = "%Y-%m-%d %H:%i";
scheduler.init("scheduler_here", new Date(2019, 0, 20), "week");
// load data from backend
scheduler.load("/api/events", "json");
// connect backend to scheduler
var dp = scheduler.createDataProcessor("/api/events");
dp.init(scheduler);
// set data exchange mode
dp.setTransactionMode("REST");
Теперь всё готово. После запуска приложения будет отображаться полностью функциональный Scheduler.
В данный момент при каждом вызове действия GET
scheduler получает всю таблицу событий. На начальном этапе это может быть приемлемо, но по мере использования приложения в течение нескольких месяцев объём передаваемых данных значительно увеличится.
Реализация динамической подгрузки позволяет scheduler получать только нужный диапазон событий.
На стороне клиента это активируется с помощью метода scheduler.setLoadMode
:
wwwroot/index.html
scheduler.setLoadMode("day");
// load data from backend
scheduler.load("/api/events", "json");
После этого scheduler будет добавлять параметры дат from
и to
к запросам GET
, чтобы сервер возвращал только события в этом диапазоне.
На сервере достаточно принять эти параметры в действии GET
и отфильтровать события соответствующим образом:
Controllers/EventsController.cs
// GET api/events
[HttpGet]
public IEnumerable<WebAPIEvent> Get([FromQuery] DateTime from, [FromQuery] DateTime to)
{
return _context.Events
.Where(e => e.StartDate < to && e.EndDate >= from)
.ToList()
.Select(e => (WebAPIEvent)e);
}
Для поддержки повторяющихся событий (например, "повторять событие ежедневно") необходимо включить соответствующее расширение на странице scheduler:
scheduler.plugins({
recurring: true
});
Модель необходимо обновить для корректной обработки информации о повторяющихся событиях:
Models/SchedulerEvent.cs
using System;
namespace SchedulerApp.Models
{
public class SchedulerEvent
{
public int Id { get; set; }
public string? Name { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int EventPID { get; set; } public string? RecType { get; set; } public long EventLength { get; set; }
}
}
Аналогично, объект передачи данных должен быть обновлён следующим образом:
Models/WebAPIEvent.cs
using System;
using System.Text.Encodings.Web;
namespace SchedulerApp.Models
{
public class WebAPIEvent
{
public int id { get; set; }
public string? text { get; set; }
public string? start_date { get; set; }
public string? end_date { get; set; }
public int? event_pid { get; set; } public string? rec_type { get; set; } public long? event_length { get; set; }
public static explicit operator WebAPIEvent(SchedulerEvent ev)
{
return new WebAPIEvent
{
id = ev.Id,
text = HtmlEncoder.Default.Encode(ev.Name != null ? ev.Name : ""),
start_date = ev.StartDate.ToString("yyyy-MM-dd HH:mm"),
end_date = ev.EndDate.ToString("yyyy-MM-dd HH:mm"),
event_pid = ev.EventPID,
rec_type = ev.RecType,
event_length = ev.EventLength
};
}
public static explicit operator SchedulerEvent(WebAPIEvent ev)
{
return new SchedulerEvent
{
Id = ev.id,
Name = ev.text,
StartDate = ev.start_date != null ? DateTime.Parse(ev.start_date,
System.Globalization.CultureInfo.InvariantCulture) : new DateTime(),
EndDate = ev.end_date != null ? DateTime.Parse(ev.end_date,
System.Globalization.CultureInfo.InvariantCulture) : new DateTime(),
///
EventPID = ev.event_pid != null ? ev.event_pid.Value : 0,
EventLength = ev.event_length != null ? ev.event_length.Value : 0,
RecType = ev.rec_type
};
}
}
}
Методы PUT, POST и DELETE необходимо скорректировать для корректной работы с особыми случаями, связанными с повторяющимися событиями, как описано в разделе обработка повторяющихся событий.
Начнём с действия POST
, которое обрабатывает уникальный случай, когда удаление одного вхождения из серии повторяющихся событий требует добавления новой записи в базу данных. В этом случае клиент вызывает действие insert
:
Controllers/EventsController.cs
// POST api/events
[HttpPost]
public ObjectResult Post([FromForm] WebAPIEvent apiEvent)
{
var newEvent = (SchedulerEvent)apiEvent;
_context.Events.Add(newEvent);
_context.SaveChanges();
// удаление одного вхождения из серии повторяющихся событий
var resultAction = "inserted"; if(newEvent.RecType == "none") {
resultAction = "deleted"; }
return Ok(new
{
tid = newEvent.Id,
action = resultAction });
}
В методе PUT
важно обновлять все свойства модели. Кроме того, при изменении серии повторяющихся событий необходимо удалить все изменённые вхождения этой серии:
Controllers/EventsController.cs
// PUT api/events/5
[HttpPut("{id}")]
public ObjectResult? Put(int id, [FromForm] WebAPIEvent apiEvent)
{
var updatedEvent = (SchedulerEvent)apiEvent;
var dbEvent = _context.Events.Find(id);
if (dbEvent == null)
{
return null;
}
dbEvent.Name = updatedEvent.Name;
dbEvent.StartDate = updatedEvent.StartDate;
dbEvent.EndDate = updatedEvent.EndDate;
dbEvent.EventPID = updatedEvent.EventPID;
dbEvent.RecType = updatedEvent.RecType;
dbEvent.EventLength = updatedEvent.EventLength;
if (!string.IsNullOrEmpty(updatedEvent.RecType) && updatedEvent.RecType != "none")
{
// все изменённые вхождения должны быть удалены при обновлении серии повторяющихся событий
// https://docs.dhtmlx.com/scheduler/server_integration.html#recurringevents
_context.Events.RemoveRange(
_context.Events.Where(e => e.EventPID == id)
);
}
_context.SaveChanges();
return Ok(new
{
action = "updated"
});
}
Наконец, метод DELETE
должен обрабатывать два сценария:
event_pid
, это означает, что удаляется изменённый экземпляр повторяющейся серии. Вместо удаления записи необходимо обновить её, установив rec_type='none'
, чтобы планировщик пропустил это вхождение.Controllers/EventsController.cs
[HttpDelete("{id}")]
public ObjectResult DeleteEvent(int id)
{
var e = _context.Events.Find(id);
if (e != null)
{
// логика, специфичная для поддержки повторяющихся событий
// https://docs.dhtmlx.com/scheduler/server_integration.html#recurringevents
if (e.EventPID != default(int))
{
// удаление изменённого вхождения из серии повторяющихся событий
// Если событие с event_pid было удалено, его нужно обновить
// с rec_type==none вместо удаления.
e.RecType = "none";
}
else
{
// если удаляется серия повторяющихся событий, удаляются все изменённые вхождения серии
if (!string.IsNullOrEmpty(e.RecType) && e.RecType != "none")
{
// все изменённые вхождения должны быть удалены при обновлении серии повторяющихся событий
// https://docs.dhtmlx.com/scheduler/server_integration.html#recurringevents
_context.Events.RemoveRange(
_context.Events.Where(ev => ev.EventPID == id)
);
}
_context.Events.Remove(e);
}
_context.SaveChanges();
}
return Ok(new
{
action = "deleted"
});
}
Повторяющееся событие хранится в базе данных как одна запись, которую Scheduler разбивает на отдельные вхождения на стороне клиента. Для получения дат отдельных событий на сервере доступна вспомогательная библиотека для разбора повторяющихся событий в dhtmlxScheduler с использованием ASP.NET Core.
Эта библиотека доступна на GitHub.
Для эффективного управления ошибками следует создать middleware-класс. Этот middleware перехватывает исключения времени выполнения и формирует ответы соответствующим образом. После создания middleware его необходимо добавить в цепочку обработки запросов приложения. Действия следующие:
1. Создайте middleware-класс по шаблону в папке проекта.
Требуется установить фреймворк JSON для ASP.NET Core и HTTP-абстракции для обработки HTTP-запросов и ответов. Это можно сделать с помощью NuGet Package Manager:
Или выполнить следующие команды в консоли диспетчера пакетов:
PM> Install-Package Microsoft.AspNetCore.Http.Abstractions
PM> Install-Package Microsoft.Newtonsoft.Json
2. Найдите метод Invoke в middleware-классе и обратите внимание на вызов _next. Поскольку некоторые обработчики могут выбрасывать исключения, оберните вызов _next в блок try-catch
, чтобы перехватить ошибки и обработать их с помощью специального обработчика.
SchedulerErrorMiddleware.cs
public async Task Invoke(HttpContext httpContext)
{
try
{
await _next(httpContext);
}catch (Exception ex)
{
await HandleExceptionAsync(httpContext, ex);
}
}
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);
}
Добавьте следующий namespace в SchedulerErrorMiddleware.cs:
SchedulerErrorMiddleware.cs
using Newtonsoft.Json;
3. После подготовки middleware откройте Program.cs и зарегистрируйте его, добавив этот namespace:
Program.cs
using SchedulerApp;
Затем вызовите app.UseSchedulerErrorMiddleware() для включения его в цепочку обработки:
Program.cs
app.UseSchedulerErrorMiddleware();
Scheduler сам по себе не предоставляет встроенной защиты от распространённых угроз безопасности, таких как SQL-инъекции, XSS или CSRF-атаки. Обеспечение безопасности приложения лежит на разработчиках, реализующих серверную часть. Подробнее см. в отдельной статье.
Простой способ защититься от XSS — кодировать текстовые свойства перед отправкой их клиенту. Например, в приведённом ниже примере используется встроенный HtmlEncoder для экранирования HTML-символов в тексте события. Такой подход сохраняет данные в базе без изменений, но отправляет клиенту безопасное содержимое:
Models/WebAPIEvent.cs
using System.Text.Encodings.Web;
public static explicit operator WebAPIEvent(SchedulerEvent ev)
{
return new WebAPIEvent
{
id = ev.Id,
text = HtmlEncoder.Default.Encode(ev.Name != null ? ev.Name : ""),
start_date = ev.StartDate.ToString("yyyy-MM-dd HH:mm"),
end_date = ev.EndDate.ToString("yyyy-MM-dd HH:mm")
};
}
Альтернативно можно использовать специализированную библиотеку, например HtmlAgilityPack, чтобы полностью удалять HTML-теги при сохранении или загрузке данных.
Если все шаги по интеграции Scheduler с ASP.NET Core были выполнены, но события не отображаются на странице, обратитесь к статье Устранение проблем с интеграцией Backend. В ней содержатся рекомендации по диагностике и устранению распространённых проблем.
На данном этапе Scheduler должен быть полностью работоспособен. Полный исходный код доступен на GitHub для клонирования или скачивания в поддержку ваших проектов.
Дополнительные ресурсы включают руководства, охватывающие множество возможностей Scheduler и туториалы по интеграции Scheduler с другими backend-фреймворками.
Наверх