Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CS/Components/Pages/Index.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public partial class Index {
private static string GetMenuIcon(string actionName) => MenuIcons[actionName];

private string GoToTodayText => ActiveViewType switch {
SchedulerViewType.Week or SchedulerViewType.WorkWeek => "Go to the current Week",
SchedulerViewType.Month => "Go to the current Month",
SchedulerViewType.Day or SchedulerViewType.Timeline => "Go to Today",
SchedulerViewType.Week or SchedulerViewType.WorkWeek => "Switch to Current Week",
SchedulerViewType.Month => "Switch to Current Month",
SchedulerViewType.Day or SchedulerViewType.Timeline => "Switch to Today",
_ => "Error: Unknown view type"
};

Expand Down
230 changes: 213 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,234 @@
<!-- default badges list -->
![](https://img.shields.io/endpoint?url=https://codecentral.devexpress.com/api/v1/VersionRange/1223677914/26.1.3%2B)
[![](https://img.shields.io/badge/Open_in_DevExpress_Support_Center-FF7200?style=flat-square&logo=DevExpress&logoColor=white)](https://supportcenter.devexpress.com/ticket/details/T1328040)
![](https://img.shields.io/endpoint?url=https://codecentral.devexpress.com/api/v1/VersionRange/1211416592/25.2.6%2B)
[![](https://img.shields.io/badge/Open_in_DevExpress_Support_Center-FF7200?style=flat-square&logo=DevExpress&logoColor=white)](https://supportcenter.devexpress.com/ticket/details/T1326784)
[![](https://img.shields.io/badge/📖_How_to_use_DevExpress_Examples-e9f6fc?style=flat-square)](https://docs.devexpress.com/GeneralInformation/403183)
[![](https://img.shields.io/badge/💬_Leave_Feedback-feecdd?style=flat-square)](#does-this-example-address-your-development-requirementsobjectives)
<!-- default badges end -->
# Product/Platform - Task
# Blazor Scheduler — Custom Context Menu for Scheduler Regions

This is the repository template for creating new examples. Describe the solved task here.
This example adds a DevExpress Blazor [Context Menu](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxContextMenu) to the DevExpress Blazor [Scheduler](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxScheduler). When users right-click within any Scheduler region, the application detects the clicked region and displays a context menu with relevant commands.

Put a screenshot that illustrates the result here.
| Scheduler Region | Context Menu Commands |
|---|---|
| Appointment | Edit |
| All Day Area | Switch to Day View *(only if `ActiveViewType != Day`)* <br/> Switch to Today |
| Time Cell | Switch to Today <br/> Switch to Day View *(only if `ActiveViewType != Day`)* |
| Date Header | Switch to Day View *(only if `ActiveViewType != Day`)* <br/> Switch to Today |
| Day of Week Header | Switch to Today |
| Resource Header | Hide Resource *(only if visible resource count > 1)* <br/> Show All Resources |
| Time Ruler | Toggle Work Time |
| Toolbar | Switch to Day View *(if not `Day`)* <br/> Switch to Week View *(if not `Week`)* <br/> Switch to Work Week View *(if not `WorkWeek`)* <br/> Switch to Month View *(if not `Month`)* <br/> Switch to Timeline View *(if not `Timeline`)* <br/> Switch to Today |

Then, add implementation details (steps, code snippets, and other technical information in a free form), or add a link to an existing document with implementation details.
Toolbar menu:

![Toolbar Context Menu](toolbar-menu.png)

Resource header menu:

![Resource Header Context Menu](resource-header-menu.png)

## Implementation Details

### Detect a Clicked Region and Display the Menu

The application uses a combination of Blazor and JavaScript to detect the clicked region and display the context menu. For [appointments](#appointments), a shared appointment template identifies the clicked appointment. For [other regions and elements](#other-regions), a JavaScript module handles the `contextmenu` event and calls back into .NET to display the menu.

If you need to add a context menu to an element that supports templates, you can use the approach used for appointments. If you need to add a context menu to an element that does not support templates, you can use the same approach as this application uses for other regions.

#### Appointments

To detect a clicked appointment, `Index.razor` defines a shared [appointmentTemplate](CS/Components/Pages/Index.razor#L9) object. The template is reused across all Scheduler views (Day, Week, Work Week, Month, and Timeline).

The template's `context` parameter contains an `Appointment` property that gets the current appointment. The template handles right-clicks on the appointment (`@oncontextmenu` event) and calls [ShowAppointmentContextMenu(e, context.Appointment)](CS/Components/Pages/Index.razor.cs#L99).


**Index.razor**
```razor
@{
RenderFragment<DxSchedulerAppointmentView> appointmentTemplate = context => @<div class="card @context.Label?.BackgroundCssClass">
<div @oncontextmenu="((e) => ShowAppointmentContextMenu(e, context.Appointment))">
@context.Appointment.Subject
</div>
</div>;
}
```

**Index.razor.cs**
```csharp
private async Task ShowAppointmentContextMenu(MouseEventArgs e, DxSchedulerAppointmentItem appointment) {
if(ContextMenu is null || appointment is null)
return;

ClickedRegion = "Appointment";
ContextMenuAppointment = appointment;
await ContextMenu.ShowAsync(e);
}

```

#### Other Regions

The example relies on the [appointmentContextMenu.js](CS/wwwroot/js/appointmentContextMenu.js) module to process the following Scheduler regions: date header, time cell, resource header, all-day cell, and day-of-week header. The module handles the browser's `contextmenu` event, determines region by a CSS class, and displays the appropriate menu on the .NET side of the application.

The [OnHtmlCellDecoration](CS/Components/Pages/Index.razor.cs#L160) event handler is used to apply custom CSS classes to Scheduler regions.

**Index.razor.cs**
```csharp
private void OnHtmlCellDecoration(SchedulerHtmlCellDecorationEventArgs e) {
switch(e.CellType) {
case SchedulerCellType.DateHeader:
e.CssClass = "custom-date-header";
break;
case SchedulerCellType.TimeCell:
e.CssClass = "custom-time-cell";
break;
case SchedulerCellType.ResourceHeader:
e.CssClass = "custom-resource-header-" + e.Resources.FirstOrDefault()?.Id;
break;
case SchedulerCellType.AllDayTimeCell:
e.CssClass = "custom-all-date-time-cell";
break;
case SchedulerCellType.DayOfWeekHeader:
e.CssClass = "custom-day-of-week-header";
break;
case SchedulerCellType.None:
e.CssClass = "custom-none";
break;
}
}
```

The [getRegion](CS/wwwroot/js/appointmentContextMenu.js) method determines an event target, matches region CSS classes, and passes region name (along with an optional resource `id` and cell `start_date`) back to the Scheduler component:

**appointmentContextMenu.js**
```js
export function setup(schedulerElement, dotNetRef) {
schedulerElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
const region = getRegion(e.target, schedulerElement);
if (!region) return;
dotNetRef.invokeMethodAsync('ShowAppointmentContextMenu',
e.clientX, e.clientY, e.pageX, e.pageY, region.name, region.id, region.start_date);
});
}
```

When a region is detected, the module calls the [[JSInvokable] ShowAppointmentContextMenu](CS/Components/Pages/Index.razor.cs#L83) method which then sets the `ClickedRegion` value and displays the menu at the cursor position.

**Index.razor.cs**
```csharp
[JSInvokable]
public async Task ShowAppointmentContextMenu(double clientX, double clientY, double pageX, double pageY, string region, int? id, long? startDate) {
ClickedRegion = region;
ClickedId = id;
DayToGo = startDate.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(startDate.Value).DateTime : null;

StateHasChanged();

await (ContextMenu?.ShowAsync(new MouseEventArgs {
ClientX = clientX,
ClientY = clientY,
PageX = pageX,
PageY = pageY
}) ?? Task.FromResult(false));
}
```

> [!NOTE]
> The [appointmentContextMenu.js](CS/wwwroot/js/appointmentContextMenu.js) module relies on DevExpress internal CSS classes (`dxbl-sc-*`, `dxbl-v-*`). These classes may change between release cycles. Review and update them as necessary when/if you upgrade DevExpress-powered Blazor app.

### Region-Aware Menu Commands

[Index.razor](CS/Components/Pages/Index.razor#L83) declares a single [DxContextMenu](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxContextMenu) used for all regions. Its items are generated dynamically based on the `ClickedRegion` value. A `switch` block renders [DxContextMenuItem](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxContextMenuItem) items applicable to a clicked region and the current view. For example, "Open this day in Day View" is hidden when the Day View is active.

**Index.razor**
```razor
<DxContextMenu @ref="@ContextMenu" ItemClick="@OnItemClick">
<Items>
<DxContextMenuItem Text="@ClickedRegion" Enabled="false" CssClass="fw-bold"></DxContextMenuItem>
@switch (ClickedRegion)
{
case "Appointment":
<DxContextMenuItem Text="Edit" Name="Edit" IconUrl="@GetMenuIcon("Edit")"></DxContextMenuItem>
break;
case "All Day Area":
@if (ActiveViewType != SchedulerViewType.Day) {
<DxContextMenuItem Text="@OpenDayInDayViewText" Name="SwitchToDayView" IconUrl="@GetMenuIcon("SwitchToDayView")"></DxContextMenuItem>
}
<DxContextMenuItem Text="@GoToTodayText" Name="GoToToday" IconUrl="@GetMenuIcon("GoToToday")"></DxContextMenuItem>
break;
case "Time Cell":
<DxContextMenuItem Text="@GoToTodayText" Name="GoToToday" IconUrl="@GetMenuIcon("GoToToday")"></DxContextMenuItem>
@if (ActiveViewType != SchedulerViewType.Day) {
<DxContextMenuItem Text="@OpenDayInDayViewText" Name="SwitchToDayView" IconUrl="@GetMenuIcon("SwitchToDayView")"></DxContextMenuItem>
}
break;
// other regions
}
</Items>
</DxContextMenu>
```

### Handle Context Menu Clicks

The [OnItemClick](CS/Components/Pages/Index.razor.cs#115) event handler processes item clicks based on command `Name` value:

- `Edit` — opens the appointment edit form using `ShowAppointmentEditFormAsync`.
- `SwitchToDayView` / `SwitchToWeekView` / `SwitchToWorkWeekView` / `SwitchToMonthView` — changes `ActiveViewType` and navigates to the clicked day (if available).
- `GoToToday` — resets `StartDate` to `DateTime.Today`.
- `HideResource` / `ShowAllResources` — updates the `VisibleResources` collection bound to `VisibleResourcesDataSource`.
- `ToggleWorkTime` — toggles the `ShowWorkTimeOnly` option across views.

**Index.razor.cs**
```csharp
private async Task OnItemClick(ContextMenuItemClickEventArgs args) {
switch(args.ItemInfo.Name) {
case "Edit":
if(Scheduler is null || ContextMenuAppointment is null)
break;

await Scheduler.ShowAppointmentEditFormAsync(false, ContextMenuAppointment);
break;
case "GoToToday":
StartDate = DateTime.Today;
break;
case "SwitchToDayView":
ActiveViewType = SchedulerViewType.Day;
if(DayToGo is not null)
StartDate = DayToGo.Value;
break;
// other commands
}

StateHasChanged();
}
```
Comment thread
Schullz marked this conversation as resolved.

## Files to Review

- link.cs (VB: link.vb)
- link.js
- ...
- [Index.razor](CS/Components/Pages/Index.razor)
- [Index.razor.cs](CS/Components/Pages/Index.razor.cs)
- [Index.razor.css](CS/Components/Pages/Index.razor.css)
- [appointmentContextMenu.js](CS/wwwroot/js/appointmentContextMenu.js)
- [Program.cs](CS/Program.cs)
- [RecurringAppointmentCollection.cs](CS/Data/RecurringAppointmentCollection.cs)
- [ResourceCollection.cs](CS/Data/ResourceCollection.cs)

## Documentation

- link
- link
- ...
- [DevExpress Blazor Scheduler](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxScheduler)
- [DevExpress Blazor Scheduler - Appointments](https://docs.devexpress.com/Blazor/403663/scheduler/appointments)
- [DevExpress Blazor Context Menu](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxContextMenu)
- [Call JavaScript functions from .NET methods (Microsoft)](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet)

Comment thread
MargaritaLoseva marked this conversation as resolved.
## Related Examples

## More Examples
- [Blazor Scheduler - Get Started](https://github.com/DevExpress-Examples/blazor-scheduler-get-started)

- link
- link
- ...
<!-- feedback -->
## Does This Example Address Your Development Requirements/Objectives?

[<img src="https://www.devexpress.com/support/examples/i/yes-button.svg"/>](https://www.devexpress.com/support/examples/survey.xml?utm_source=github&utm_campaign=draft-Devexpress-Blazor-Scheduler-Context-Menu&~~~was_helpful=yes) [<img src="https://www.devexpress.com/support/examples/i/no-button.svg"/>](https://www.devexpress.com/support/examples/survey.xml?utm_source=github&utm_campaign=draft-Devexpress-Blazor-Scheduler-Context-Menu&~~~was_helpful=no)
[<img src="https://www.devexpress.com/support/examples/i/yes-button.svg"/>](https://www.devexpress.com/support/examples/survey.xml?utm_source=github&utm_campaign=blazor-scheduler-context-menu&~~~was_helpful=yes) [<img src="https://www.devexpress.com/support/examples/i/no-button.svg"/>](https://www.devexpress.com/support/examples/survey.xml?utm_source=github&utm_campaign=blazor-scheduler-context-menu&~~~was_helpful=no)

(you will be redirected to DevExpress.com to submit your response)
<!-- feedback end -->
Binary file added resource-header-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added toolbar-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading