Skip to content

Commit 6136063

Browse files
Merge pull request #52 from AlexKlimenkov/master
[dev] remove code duplication from blazor guide, move blazor guide on…
2 parents 68ced96 + 07097d7 commit 6136063

2 files changed

Lines changed: 12 additions & 238 deletions

File tree

docs/integrations/dotnet/howtostart-blazor.md

Lines changed: 9 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,6 @@ sidebar_label: "Blazor"
77

88
This tutorial gives you step-by-step instructions on how to host Scheduler inside a Blazor Web App. Blazor renders the host page, [JavaScript interop](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/) initializes the Scheduler widget, and an ASP.NET Core Web API controller serves CRUD requests.
99

10-
You can also read tutorials on other server-side technologies:
11-
12-
- [dhtmlxScheduler with ASP.NET Core](integrations/dotnet/howtostart-dotnet-core.md)
13-
- [dhtmlxScheduler with ASP.NET MVC](integrations/dotnet/howtostart-dotnet.md)
14-
- [dhtmlxScheduler with Node.js](integrations/node/howtostart-nodejs.md)
15-
- [dhtmlxScheduler with FastAPI](integrations/python/howtostart-fastapi.md)
16-
- [dhtmlxScheduler with PHP](integrations/php/howtostart-plain-php.md)
17-
- [dhtmlxScheduler with PHP:Slim](integrations/php/howtostart-php-slim4.md)
18-
- [dhtmlxScheduler with PHP:Laravel](integrations/php/howtostart-php-laravel.md)
19-
- [dhtmlxScheduler with SalesForce LWC](integrations/salesforce/howtostart-salesforce.md)
20-
- [dhtmlxScheduler with Ruby on Rails](integrations/other/howtostart-ruby.md)
21-
- [dhtmlxScheduler with dhtmlxConnector](integrations/other/howtostart-connector.md)
22-
2310
The application is built with the .NET 10 SDK and Visual Studio Code with the [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit). Visual Studio 2022 works as well. To keep the example focused on Scheduler integration, the demo stores events in an in-memory list rather than a database; for a persistent storage example see the [ASP.NET Core integration guide](integrations/dotnet/howtostart-dotnet-core.md#step-3-creating-models-and-database).
2411

2512
:::note
@@ -117,14 +104,7 @@ Next steps will show you how to create a backend API and connect Scheduler to it
117104

118105
## Step 3. Creating models
119106

120-
Let's begin with a data model. You'll need a class for scheduler events. dhtmlxScheduler uses [non-conventional names for model properties](guides/data-formats.md#json) from the .NET world perspective.
121-
122-
To deal with this, the [Data Transfer Object (DTO)](https://learn.microsoft.com/en-us/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5) pattern will be used. Two kinds of models will be defined:
123-
124-
- a domain model class that will be used inside the app
125-
- a DTO class that will be used to communicate with the Web API.
126-
127-
Then mapping between the two models is implemented as explicit conversion operators.
107+
Let's begin with a data model. You'll need a class for scheduler events. dhtmlxScheduler uses [non-conventional names for model properties](guides/data-formats.md#json) from the .NET world perspective. This uses the same DTO pattern explained in the [ASP.NET Core tutorial](integrations/dotnet/howtostart-dotnet-core.md#define-dtos-and-mapping); the Blazor-adapted version is below.
128108

129109
Create a **Models** folder in the project root. First, the domain model:
130110

@@ -321,223 +301,17 @@ The default `date_format` config string can't express the ISO `T` delimiter, so
321301

322302
Everything is ready. You can run the application and see the fully-fledged Scheduler.
323303

324-
## Dynamic loading
325-
326-
Each time scheduler calls our `GET` action, it loads the whole events list. It may be ok for a start, but after the app is used for several months the amount of data transferred will grow dramatically. So it may be worthwhile to implement dynamic loading, which allows scheduler to load only a required events range.
327-
328-
On the client side it is enabled by the [scheduler.setLoadMode](api/method/setloadmode.md) method:
329-
330-
~~~js title="wwwroot/lib/scheduler/scheduler.js"
331-
scheduler.setLoadMode("day");
332-
// load data from backend
333-
scheduler.load("/api/scheduler");
334-
~~~
335-
336-
After that scheduler will start adding `from` and `to` date parameters to GET requests, so the backend can return only events between these dates.
337-
338-
All we need to do is to read those parameters in our `GET` action and filter events appropriately:
339-
340-
~~~csharp title="Controllers/SchedulerController.cs"
341-
[HttpGet]
342-
public IActionResult Get([FromQuery] DateTime? from, [FromQuery] DateTime? to)
343-
{
344-
IEnumerable<SchedulerEvent> filteredEvents = _events;
345-
346-
if (from.HasValue && to.HasValue)
347-
{
348-
filteredEvents = _events.Where(e =>
349-
e.StartDate < to.Value &&
350-
e.EndDate > from.Value);
351-
}
352-
353-
var result = filteredEvents.Select(e => (WebAPIEvent)e).ToList();
354-
return Ok(result);
355-
}
356-
~~~
357-
358-
## Recurring events
359-
360-
In order to enable recurrence (e.g. "repeat event daily") you'll need to enable an appropriate extension on the scheduler page:
361-
362-
~~~js title="wwwroot/lib/scheduler/scheduler.js"
363-
scheduler.plugins({
364-
recurring: true
365-
});
366-
~~~
367-
368-
### Updating the model
369-
370-
We also need to update our model in order for it to store recurrence info:
371-
372-
~~~csharp title="Models/SchedulerEvent.cs"
373-
namespace BlazorApp.Models;
374-
375-
public class SchedulerEvent
376-
{
377-
public int Id { get; set; }
378-
public string? Text { get; set; }
379-
public DateTime StartDate { get; set; }
380-
public DateTime EndDate { get; set; }
381-
public int? Duration { get; set; } /*!*/
382-
public string? Rrule { get; set; } /*!*/
383-
public string? RecurringEventId { get; set; } /*!*/
384-
public string? OriginalStart { get; set; } /*!*/
385-
public bool? Deleted { get; set; } /*!*/
386-
}
387-
~~~
388-
389-
And the data transfer object:
390-
391-
~~~csharp title="Models/WebAPIEvent.cs"
392-
namespace BlazorApp.Models;
393-
394-
public class WebAPIEvent
395-
{
396-
public int id { get; set; }
397-
public string? text { get; set; }
398-
public string? start_date { get; set; }
399-
public string? end_date { get; set; }
400-
public int? duration { get; set; } /*!*/
401-
public string? rrule { get; set; } /*!*/
402-
public string? recurring_event_id { get; set; } /*!*/
403-
public string? original_start { get; set; } /*!*/
404-
public bool? deleted { get; set; } /*!*/
405-
406-
public static explicit operator WebAPIEvent(SchedulerEvent ev)
407-
{
408-
return new WebAPIEvent
409-
{
410-
id = ev.Id,
411-
text = ev.Text,
412-
start_date = ev.StartDate.ToString("yyyy-MM-ddTHH:mm:ss"),
413-
end_date = ev.EndDate.ToString("yyyy-MM-ddTHH:mm:ss"),
414-
duration = ev.Duration, /*!*/
415-
rrule = ev.Rrule, /*!*/
416-
recurring_event_id = ev.RecurringEventId, /*!*/
417-
original_start = ev.OriginalStart, /*!*/
418-
deleted = ev.Deleted /*!*/
419-
};
420-
}
421-
422-
public static explicit operator SchedulerEvent(WebAPIEvent ev)
423-
{
424-
return new SchedulerEvent
425-
{
426-
Id = ev.id,
427-
Text = ev.text,
428-
StartDate = ev.start_date != null ? DateTime.Parse(ev.start_date) : DateTime.UtcNow,
429-
EndDate = ev.end_date != null ? DateTime.Parse(ev.end_date) : DateTime.UtcNow,
430-
Duration = ev.duration, /*!*/
431-
Rrule = ev.rrule, /*!*/
432-
RecurringEventId = ev.recurring_event_id, /*!*/
433-
OriginalStart = ev.original_start, /*!*/
434-
Deleted = ev.deleted ?? false /*!*/
435-
};
436-
}
437-
}
438-
~~~
439-
440-
### Updating the API controller
441-
442-
Lastly, we need to modify our PUT/POST/DELETE actions in order to [handle special rules of recurring events](guides/recurring-events.md#editingdeleting-a-certain-occurrence-in-the-series).
304+
## See also
443305

444-
Firstly, the `POST` action. We need to process a special case for recurring events - deletion of a specific occurrence of the recurring series requires creating a new record and the client will call the `insert` action for it:
306+
The Web API controller in this tutorial is a basic CRUD skeleton on top of in-memory storage. The topics below apply equally to a Blazor host but are not duplicated here - follow the ASP.NET Core integration guide:
445307

446-
~~~csharp title="Controllers/SchedulerController.cs"
447-
[HttpPost]
448-
public IActionResult Post([FromBody] WebAPIEvent dto)
449-
{
450-
if (!ModelState.IsValid) return BadRequest();
451-
var evt = (SchedulerEvent)dto;
452-
var action = "inserted";
453-
if (dto.deleted == true) /*!*/
454-
{
455-
// Delete a single occurrence from a recurring series
456-
action = "deleted"; /*!*/
457-
}
458-
evt.Id = _nextId++;
459-
_events.Add(evt);
460-
return Ok(new { tid = evt.Id, action });
461-
}
462-
~~~
308+
- [Dynamic loading](integrations/dotnet/howtostart-dotnet-core.md#dynamic-loading) - only fetch events in the visible date range
309+
- [Recurring events](integrations/dotnet/howtostart-dotnet-core.md#recurring-events) - model, DTO, and POST/PUT/DELETE handling
310+
- [Error handling middleware](integrations/dotnet/howtostart-dotnet-core.md#error-handling)
311+
- [Application security / XSS protection](integrations/dotnet/howtostart-dotnet-core.md#application-security)
312+
- [Troubleshooting backend integration](guides/troubleshooting.md)
463313

464-
In the `PUT` action we need to make sure to update all properties of the model. Additionally, we need to handle a different special case there: when a recurring series is modified, we need to delete all modified occurrences of that series:
465-
466-
~~~csharp title="Controllers/SchedulerController.cs"
467-
[HttpPut("{id:int}")]
468-
public IActionResult Put(int id, [FromBody] WebAPIEvent dto)
469-
{
470-
if (!ModelState.IsValid) return BadRequest();
471-
var index = _events.FindIndex(e => e.Id == id);
472-
if (index == -1) return NotFound();
473-
474-
var evt = _events[index];
475-
evt.Text = dto.text;
476-
evt.StartDate = (dto.start_date != null) ? DateTime.Parse(dto.start_date) : evt.StartDate;
477-
evt.EndDate = (dto.end_date != null) ? DateTime.Parse(dto.end_date) : evt.EndDate;
478-
evt.Duration = dto.duration; /*!*/
479-
evt.Rrule = dto.rrule; /*!*/
480-
evt.RecurringEventId = dto.recurring_event_id; /*!*/
481-
evt.OriginalStart = dto.original_start; /*!*/
482-
evt.Deleted = dto.deleted ?? false; /*!*/
483-
484-
// If editing the series (rrule present, no recurring_event_id),
485-
// delete all modified occurrences of that series.
486-
if (!string.IsNullOrEmpty(evt.Rrule) && string.IsNullOrEmpty(evt.RecurringEventId)) /*!*/
487-
{
488-
var children = _events
489-
.Where(e => !string.IsNullOrEmpty(e.RecurringEventId)
490-
&& e.RecurringEventId == evt.Id.ToString())
491-
.ToList();
492-
foreach (var child in children)
493-
{
494-
_events.Remove(child);
495-
}
496-
}
497-
498-
return Ok(new { action = "updated" });
499-
}
500-
~~~
501-
502-
And finally, the `DELETE` action. Two special cases:
503-
504-
- if the event you are going to delete is a modified instance of the recurring series, instead of deleting, update the record to mark `deleted`.
505-
- if a user deletes a whole recurring series, you also need to delete all the modified instances of that series.
506-
507-
~~~csharp title="Controllers/SchedulerController.cs"
508-
[HttpDelete("{id:int}")]
509-
public IActionResult Delete(int id)
510-
{
511-
var evtIndex = _events.FindIndex(e => e.Id == id);
512-
if (evtIndex == -1) return NotFound();
513-
514-
var evt = _events[evtIndex];
515-
516-
if (!string.IsNullOrEmpty(evt.RecurringEventId)) /*!*/
517-
{
518-
// Deleting modified occurrence: soft-delete (set deleted = true)
519-
evt.Deleted = true; /*!*/
520-
}
521-
else
522-
{
523-
// Deleting series or standalone event: remove children + itself
524-
if (!string.IsNullOrEmpty(evt.Rrule)) /*!*/
525-
{
526-
var children = _events
527-
.Where(e => !string.IsNullOrEmpty(e.RecurringEventId)
528-
&& e.RecurringEventId == evt.Id.ToString())
529-
.ToList();
530-
foreach (var child in children)
531-
{
532-
_events.Remove(child);
533-
}
534-
}
535-
_events.RemoveAt(evtIndex);
536-
}
537-
538-
return Ok(new { action = "deleted" });
539-
}
540-
~~~
314+
For a persistent backend (Entity Framework Core + SQL Server), the [ASP.NET Core tutorial](integrations/dotnet/howtostart-dotnet-core.md) covers the full setup.
541315

542316
## What's next
543317

sidebars.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,9 @@ module.exports = {
122122
"integrations/angular/howtostart-angular",
123123
"integrations/svelte/howtostart-svelte",
124124
"integrations/salesforce/howtostart-salesforce",
125-
"integrations/google-calendar/google-calendar-sync",
126-
"integrations/firebase/howtostart-firebase",
125+
"integrations/google-calendar/google-calendar-sync",
126+
"integrations/dotnet/howtostart-blazor",
127+
"integrations/firebase/howtostart-firebase",
127128
{
128129
type: "category",
129130
label: "Backends",
@@ -136,7 +137,6 @@ module.exports = {
136137
"integrations/node/howtostart-nodejs",
137138
"integrations/python/howtostart-fastapi",
138139
"integrations/dotnet/howtostart-dotnet-core",
139-
"integrations/dotnet/howtostart-blazor",
140140
"integrations/php/howtostart-php-laravel",
141141
"integrations/dotnet/howtostart-dotnet",
142142
"integrations/php/howtostart-php-slim4",

0 commit comments

Comments
 (0)