Backend API
@model DevExtreme.NETCore.Demos.ViewModels.SchedulerResourcesViewModel
@(Html.DevExtreme().Scheduler()
.ID("scheduler")
.DataSource(Model.Appointments)
.StartDateExpr("StartDate")
.EndDateExpr("EndDate")
.Views(new[] {
SchedulerViewType.Day,
SchedulerViewType.Week,
SchedulerViewType.WorkWeek,
SchedulerViewType.Month
})
.CurrentView(SchedulerViewType.Week)
.CurrentDate(new DateTime(2026, 2, 10))
.FirstDayOfWeek(DayOfWeek.Sunday)
.StartDayHour(9)
.EndDayHour(19)
.ShowAllDayPanel(false)
.AllDayPanelMode(AllDayPanelMode.Hidden)
.Height(600)
.TextExpr("Text")
.StartDateExpr("StartDate")
.EndDateExpr("EndDate")
.AllDayExpr("AllDay")
.RecurrenceRuleExpr("RecurrenceRule")
.RecurrenceExceptionExpr("RecurrenceException")
.Resources(res =>
{
res.Add()
.FieldExpr("AssigneeId")
.DataSource(Model.Assignees)
.ValueExpr("Id")
.DisplayExpr("Text")
.ColorExpr("Color")
.Icon("user")
.AllowMultiple(true);
})
.Editing(e => e
.Popup(popup => popup
.OnInitialized("onPopupInitialized")
.OnHidden("onPopupHidden")
)
.Form(form => form
.LabelMode(FormLabelMode.Hidden)
.ElementAttr(new { @class = "hide-informer", id = "form" })
.OnInitialized("onFormInitialized")
.Items(items =>
{
items.AddSimple()
.Name("conflictInformer")
.Template(new JS("conflictInformerTemplate"));
items.AddGroup()
.Name("mainGroup")
.Items(groupItems =>
{
groupItems.AddGroup()
.Name("subjectGroup");
groupItems.AddGroup()
.Name("dateGroup");
groupItems.AddGroup()
.Name("repeatGroup");
groupItems.AddGroup()
.Name("AssigneeIdGroup")
.Items(assigneeItems =>
{
assigneeItems.AddSimple()
.Name("AssigneeIdIcon");
assigneeItems.AddSimple()
.Name("AssigneeIdEditor")
.IsRequired(true)
.Editor(e => e.TagBox()
.OnValueChanged("onAssigneeValueChanged")
.TagTemplate(new JS("tagTemplate"))
);
});
});
items.AddGroup()
.Name("recurrenceGroup");
})
.CustomizeItem("customizeFormItem")
)
)
.OnAppointmentAdding("onAppointmentAdding")
.OnAppointmentUpdating("onAppointmentUpdating")
)
<div class="options">
<div class="option">
<span>Overlapping Rule</span>
@(Html.DevExtreme().SelectBox()
.ID("calendar-container")
.ValueExpr("value")
.DisplayExpr("text")
.Value("sameResource")
.DataSource(new[] {
new { text= "Allow across resources", value= "sameResource" },
new { text= "Disallow all overlaps", value= "allResources" }
})
.OnValueChanged("onSelectBoxValueChanged")
)
<div id="overlapping-rule"></div>
</div>
</div>
<script>
let popup;
let form;
let showConflictError = false;
let overlappingRule = 'sameResource';
function onSelectBoxValueChanged(e) {
overlappingRule = e.value;
}
function conflictInformerTemplate() {
return $('<div>')
.addClass('conflict-informer')
.text('This time slot conflicts with another appointment.');
}
function tagTemplate(tagData) {
return $('<div />')
.css('background-color', tagData.Color)
.css('border-color', tagData.Color)
.addClass('dx-tag-content')
.append(
$('<span />').text(tagData.Text),
$('<div />').addClass('dx-tag-remove-button'),
);
}
function onPopupInitialized(e) {
popup = e.component;
}
function onPopupHidden() {
setConflictError(false);
}
function onFormInitialized(e) {
form = e.component;
form.on('fieldDataChanged', function(e) {
if (showConflictError && ['StartDate', 'EndDate', 'AssigneeId', 'RecurrenceRule'].includes(e.dataField)) {
setConflictError(false);
form.validate();
}
});
}
function onAssigneeValueChanged(e) {
if (e.value && e.value.length > 1) {
e.component.option('value', [e.value[e.value.length - 1]]);
}
}
function customizeFormItem(item) {
if (item.name === 'allDayEditor' || item.name === 'recurrenceEndEditor') {
item.label.visible = true;
} else if (item.name === 'subjectEditor') {
item.editorOptions = item.editorOptions || {};
item.editorOptions.placeholder = 'Add title';
}
if (item.name === 'startTimeEditor' || item.name === 'endTimeEditor') {
item.validationRules = [
{ type: 'required' },
{
type: 'custom',
message: 'Time conflict',
ignoreEmptyValue: true,
reevaluate: true,
validationCallback: function() {
return !showConflictError;
}
}
];
}
}
function onAppointmentAdding(e) {
console.log(e);
alertConflictIfNeeded(e, e.appointmentData);
}
function onAppointmentUpdating(e) {
console.log(e);
alertConflictIfNeeded(e, e.newData);
}
function setConflictError(show) {
showConflictError = show;
if (form) {
form.option('elementAttr.class', show ? '' : 'hide-informer');
}
}
function alertConflictIfNeeded(e, appointmentData) {
if (!detectConflict(e.component, appointmentData)) {
console.log('no conflict');
setConflictError(false);
return;
}
e.cancel = true;
if (popup && popup.option('visible')) {
setConflictError(true);
form.validate();
} else {
const dialog = DevExpress.ui.dialog.custom({
showTitle: false,
messageHtml: '<p id="conflict-dialog">This time slot conflicts with another appointment.</p>',
buttons: [{
type: 'default',
text: 'Close',
stylingMode: 'contained',
onClick: function() {
dialog.hide();
}
}]
});
dialog.show();
}
}
function getNextDay(date) {
const next = new Date(date);
next.setDate(next.getDate() + 1);
return next;
}
function getEndDate(occurrence) {
return occurrence.appointmentData.AllDay
? getNextDay(occurrence.StartDate)
: occurrence.endDate;
}
function isOverlapping(a, b) {
const aEnd = getEndDate(a);
const bEnd = getEndDate(b);
if (a.startDate >= bEnd || b.startDate >= aEnd) {
return false;
}
if (overlappingRule === 'sameResource') {
return a.appointmentData.AssigneeId[0] === b.appointmentData.AssigneeId[0];
}
return true;
}
function detectConflict(scheduler, newAppointment) {
const allAppointments = scheduler.getDataSource().items();
const startDate = new Date(newAppointment.StartDate);
let endDate;
if (newAppointment.RecurrenceRule) {
endDate = scheduler.getEndViewDate();
} else if (newAppointment.AllDay) {
endDate = getNextDay(startDate);
} else {
endDate = new Date(newAppointment.EndDate);
}
const existingOccurrences = scheduler
.getOccurrences(startDate, endDate, allAppointments)
.filter(function(occurrence) {
return occurrence.appointmentData.AppointmentId !== newAppointment.AppointmentId;
});
const newOccurrences = scheduler.getOccurrences(startDate, endDate, [newAppointment]);
return newOccurrences.some(function(newOccurrence) {
return existingOccurrences.some(function(existingOccurrence) {
return isOverlapping(newOccurrence, existingOccurrence);
});
});
}
</script>
using Microsoft.AspNetCore.Mvc;
using DevExtreme.NETCore.Demos.Models.SampleData;
using DevExtreme.NETCore.Demos.Models.Scheduler.ResolveTimeConflicts;
using DevExtreme.NETCore.Demos.ViewModels;
namespace DevExtreme.NETCore.Demos.Controllers {
public class SchedulerController : Controller {
public ActionResult ResolveTimeConflicts() {
return View(new SchedulerResourcesViewModel {
Appointments = ResolveTimeConflictsData.Appointments,
Assignees = ResolveTimeConflictsData.AssigneeResources,
});
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using DevExtreme.NETCore.Demos.Models.SampleData;
namespace DevExtreme.NETCore.Demos.Models.Scheduler.ResolveTimeConflicts {
public class ResolveTimeConflictsData {
public static readonly IEnumerable<AssigneeResource> AssigneeResources = new[] {
new AssigneeResource {
Id = 1,
Text = "Samantha Bright",
Color = "#A7E3A5"
},
new AssigneeResource {
Id = 2,
Text = "John Heart",
Color = "#CFE4FA"
},
new AssigneeResource {
Id = 3,
Text = "Todd Hoffman",
Color = "#F9E2AE"
},
new AssigneeResource {
Id = 4,
Text = "Sandra Johnson",
Color = "#F1BBBC"
}
};
public static readonly IEnumerable<AppointmentWithResources> Appointments = new[] {
new AppointmentWithResources {
AppointmentId = 1,
Text = "Website Re-Design Plan",
AssigneeId = new int[] { 2 },
StartDate = "2026-02-09T09:30:00.000",
EndDate = "2026-02-09T11:30:00.000"
},
new AppointmentWithResources {
AppointmentId = 2,
Text = "Install New Router in Dev Room",
AssigneeId = new int[] { 3 },
StartDate = "2026-02-09T14:30:00.000",
EndDate = "2026-02-09T15:30:00.000"
},
new AppointmentWithResources {
AppointmentId = 3,
Text = "Approve Personal Computer Upgrade Plan",
AssigneeId = new int[] { 1 },
StartDate = "2026-02-10T10:00:00.000",
EndDate = "2026-02-10T11:00:00.000"
},
new AppointmentWithResources {
AppointmentId = 4,
Text = "Final Budget Review",
AssigneeId = new int[] { 1 },
StartDate = "2026-02-10T12:00:00.000",
EndDate = "2026-02-10T13:35:00.000"
},
new AppointmentWithResources {
AppointmentId = 5,
Text = "Install New Database",
AssigneeId = new int[] { 4 },
StartDate = "2026-02-11T09:45:00.000",
EndDate = "2026-02-11T11:15:00.000"
},
new AppointmentWithResources {
AppointmentId = 6,
Text = "Approve New Online Marketing Strategy",
AssigneeId = new int[] { 2 },
StartDate = "2026-02-11T12:00:00.000",
EndDate = "2026-02-11T14:00:00.000"
},
new AppointmentWithResources {
AppointmentId = 7,
Text = "Prepare 2021 Marketing Plan",
AssigneeId = new int[] { 3 },
StartDate = "2026-02-12T11:00:00.000",
EndDate = "2026-02-12T13:30:00.000"
},
new AppointmentWithResources {
AppointmentId = 8,
Text = "Brochure Design Review",
AssigneeId = new int[] { 2 },
StartDate = "2026-02-12T14:00:00.000",
EndDate = "2026-02-12T15:30:00.000"
},
new AppointmentWithResources {
AppointmentId = 9,
Text = "Create Icons for Website",
AssigneeId = new int[] { 1 },
StartDate = "2026-02-13T10:00:00.000",
EndDate = "2026-02-13T11:30:00.000"
},
new AppointmentWithResources {
AppointmentId = 10,
Text = "Launch New Website",
AssigneeId = new int[] { 4 },
StartDate = "2026-02-13T12:20:00.000",
EndDate = "2026-02-13T14:00:00.000"
},
new AppointmentWithResources {
AppointmentId = 11,
Text = "Upgrade Server Hardware",
AssigneeId = new int[] { 2 },
StartDate = "2026-02-13T14:30:00.000",
EndDate = "2026-02-13T16:00:00.000"
}
};
}
}
.dx-scheduler-appointment {
color: #242424;
}
.options {
padding: 20px;
background-color: rgba(191, 191, 191, 0.15);
margin-top: 20px;
}
.option {
margin-top: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.hide-informer .dx-item:has(.conflict-informer) {
display: none !important;
}
.conflict-informer {
background-color: #FCEAE8;
color: #C50F1F;
font-size: 12px;
padding: 0 12px;
height: 36px;
line-height: 36px;
box-sizing: border-box;
margin-bottom: 8px;
}
.dx-dialog:has(#conflict-dialog) .dx-overlay-content {
width: 280px;
}
.dx-dialog:has(#conflict-dialog) .dx-dialog-content {
padding-bottom: 16px;
}
.dx-dialog:has(#conflict-dialog) .dx-dialog-buttons {
padding-top: 0;
padding-bottom: 16px;
}
.dx-dialog:has(#conflict-dialog) .dx-toolbar-center,
.dx-dialog:has(#conflict-dialog) .dx-button {
width: 100%;
}
Detect Conflicts
Handle the onAppointmentAdding and onAppointmentUpdating events to check if a new or updated appointment creates a time conflict. Set e.cancel = true to block the operation when necessary.
Call getOccurrences to expand recurring appointments into individual occurrences within the target range. Check for overlapping time range values.
Conflict Detection Modes
The demo implements the following detection modes:
- Different Resources: appointments assigned to different resources (assignees) can overlap.
- Never: overlapping appointments are not allowed, regardless of resource assignment.
To implement resource-aware checks, access appointments and compare their assigneeId field values.
Display Errors
When a conflict is detected, the demo displays the error as follows:
- A message box.
- An inline validation message (if an appointment edit form is active).
To display inline validation, configure a custom form item inside editing.form and use the customizeItem function to attach custom validationRules to time editors.