diff --git a/CS/BlazorKanban/Components/App.razor b/CS/BlazorKanban/Components/App.razor index 740f647..e455013 100644 --- a/CS/BlazorKanban/Components/App.razor +++ b/CS/BlazorKanban/Components/App.razor @@ -7,13 +7,13 @@ - @DxResourceManager.RegisterTheme(Themes.Fluent) + @DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties => { + properties.AddFilePaths("css/my-styles.css"); + properties.AddFilePaths("css/kanban.css"); + properties.AddFilePaths("css/card-styles.css"); + properties.AddFilePaths("BlazorKanban.styles.css"); + })) @DxResourceManager.RegisterScripts() - - - - - diff --git a/CS/BlazorKanban/Components/DxKanban/DxKanban.razor b/CS/BlazorKanban/Components/DxKanban/DxKanban.razor index 30b7466..fe9f906 100644 --- a/CS/BlazorKanban/Components/DxKanban/DxKanban.razor +++ b/CS/BlazorKanban/Components/DxKanban/DxKanban.razor @@ -1,4 +1,6 @@ - diff --git a/CS/BlazorKanban/Components/DxKanban/DxKanban.razor.cs b/CS/BlazorKanban/Components/DxKanban/DxKanban.razor.cs index 93f9f32..24a0396 100644 --- a/CS/BlazorKanban/Components/DxKanban/DxKanban.razor.cs +++ b/CS/BlazorKanban/Components/DxKanban/DxKanban.razor.cs @@ -6,74 +6,75 @@ using System.ComponentModel; using System.Reflection; -namespace BlazorKanban.Components.DxKanban; -public partial class DxKanban : ComponentBase, IAsyncDisposable { - #region Fields - private IEnumerable sampleSingleCellData = Enumerable.Range(0, 1); - private IJSObjectReference? jsModule; - #endregion +namespace BlazorKanban.Components.DxKanban { + public partial class DxKanban : ComponentBase, IAsyncDisposable { + #region Fields + private IEnumerable sampleSingleCellData = Enumerable.Range(0, 1); + private IJSObjectReference? jsModule; + #endregion - #region Services - [Inject] - private IJSRuntime JS { get; set; } = default!; - #endregion + #region Services + [Inject] + private IJSRuntime JS { get; set; } = default!; + #endregion - #region Parameters - [Parameter] - public IEnumerable? Data { get; set; } + #region Parameters + [Parameter] + public IEnumerable? Data { get; set; } - [Parameter] - public string? CssClass { get; set; } + [Parameter] + public string? CssClass { get; set; } - [Parameter] - public string? ColumnNameFieldName { get; set; } + [Parameter] + public string? ColumnNameFieldName { get; set; } - [Parameter] - public RenderFragment? Columns { get; set; } + [Parameter] + public RenderFragment? Columns { get; set; } - [Parameter] - public RenderFragment? CardTemplate { get; set; } + [Parameter] + public RenderFragment? CardTemplate { get; set; } - [Parameter] - public EventCallback CardDropped { get; set; } - #endregion + [Parameter] + public EventCallback CardDropped { get; set; } + #endregion - #region Event Handlers - private void ApplyCssClassesToHeaderAndDataCells(GridCustomizeElementEventArgs e) { - switch(e.ElementType) { - case GridElementType.HeaderCell: - e.CssClass = "kanban-header-cell"; - break; - case GridElementType.DataCell: - e.CssClass = "kanban-data-cell"; - break; + #region Event Handlers + private void ApplyCssClassesToHeaderAndDataCells(GridCustomizeElementEventArgs e) { + switch(e.ElementType) { + case GridElementType.HeaderCell: + e.CssClass = "kanban-header-cell"; + break; + case GridElementType.DataCell: + e.CssClass = "kanban-data-cell"; + break; + } } - } - #endregion + #endregion - #region Lifecycle Methods - protected override async Task OnAfterRenderAsync(bool firstRender) { - if(jsModule is null) { - jsModule = await JS.InvokeAsync("import", "/Components/DxKanban/DxKanban.razor.js"); + #region Lifecycle Methods + protected override async Task OnAfterRenderAsync(bool firstRender) { + if(jsModule is null) { + jsModule = await JS.InvokeAsync("import", "/Components/DxKanban/DxKanban.razor.js"); + } + await jsModule.InvokeVoidAsync("moveGridDataCellContentToAnchors"); } - await jsModule.InvokeVoidAsync("moveGridDataCellContentToAnchors"); - } - public async ValueTask DisposeAsync() { - try { - if(jsModule != null) { - await jsModule.DisposeAsync(); + public async ValueTask DisposeAsync() { + try { + if(jsModule != null) { + await jsModule.DisposeAsync(); + } } + catch(JSDisconnectedException) { } } - catch(JSDisconnectedException) { } - } - #endregion + #endregion - #region Utility Methods - public void Refresh() => StateHasChanged(); + #region Utility Methods + public void Refresh() => StateHasChanged(); - private string GetGridCssClass() { - return $"kanban-layout-grid {CssClass}"; + private string GetGridCssClass() { + return $"kanban-layout-grid {CssClass}"; + } + #endregion } - #endregion -} +} \ No newline at end of file diff --git a/CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor b/CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor index 932e70e..1bbe31e 100644 --- a/CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor +++ b/CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor @@ -1,4 +1,6 @@ - diff --git a/CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor.cs b/CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor.cs index bec4c64..8b74ea7 100644 --- a/CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor.cs +++ b/CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor.cs @@ -4,17 +4,18 @@ using Microsoft.JSInterop; using System.Reflection; -namespace BlazorKanban.Components.DxKanban; -public partial class DxKanbanColumn : ComponentBase { - #region Parameters - [CascadingParameter] - private DxKanban? Kanban { get; set; } +namespace BlazorKanban.Components.DxKanban { + public partial class DxKanbanColumn : ComponentBase { + #region Parameters + [CascadingParameter] + private DxKanban? Kanban { get; set; } - [Parameter] - public string? ColumnName { get; set; } - #endregion + [Parameter] + public string? ColumnName { get; set; } + #endregion - #region Event Handlers - protected override bool ShouldRender() => false; //t1135370 - #endregion + #region Event Handlers + protected override bool ShouldRender() => false; //t1135370 + #endregion + } } diff --git a/CS/BlazorKanban/Components/DxKanban/DxKanbanColumnGrid.razor b/CS/BlazorKanban/Components/DxKanban/DxKanbanColumnGrid.razor index ddd0170..64cff22 100644 --- a/CS/BlazorKanban/Components/DxKanban/DxKanbanColumnGrid.razor +++ b/CS/BlazorKanban/Components/DxKanban/DxKanbanColumnGrid.razor @@ -1,4 +1,6 @@ - GetDataFilteredByColumnName(string? columnName) { - if(Kanban.Data is null) { - return Array.Empty(); - } + #region Utility Methods + private IEnumerable GetDataFilteredByColumnName(string? columnName) { + if(Kanban.Data is null) { + return Array.Empty(); + } - return Kanban.Data.OfType() - .Where(item => GetColumnNameFromDataItem(item) == columnName); - } + return Kanban.Data.OfType() + .Where(item => GetColumnNameFromDataItem(item) == columnName); + } - private string? GetColumnNameFromDataItem(object item) { - ArgumentNullException.ThrowIfNullOrEmpty(Kanban.ColumnNameFieldName); - PropertyInfo columnNameProperty = item.GetType().GetProperty(Kanban.ColumnNameFieldName) - ?? throw new MissingMemberException($"The data item does not contain a property named '{Kanban.ColumnNameFieldName}'."); - return columnNameProperty.GetValue(item)?.ToString(); + private string? GetColumnNameFromDataItem(object item) { + ArgumentNullException.ThrowIfNullOrEmpty(Kanban.ColumnNameFieldName); + PropertyInfo columnNameProperty = item.GetType().GetProperty(Kanban.ColumnNameFieldName) + ?? throw new MissingMemberException($"The data item does not contain a property named '{Kanban.ColumnNameFieldName}'."); + return columnNameProperty.GetValue(item)?.ToString(); + } + #endregion } - #endregion -} +} \ No newline at end of file diff --git a/CS/BlazorKanban/Components/Pages/Index.razor b/CS/BlazorKanban/Components/Pages/Index.razor index 1c2b06d..03726cf 100644 --- a/CS/BlazorKanban/Components/Pages/Index.razor +++ b/CS/BlazorKanban/Components/Pages/Index.razor @@ -59,4 +59,4 @@ (KanbanData[targetCardIndex], KanbanData[targetCardIndex + 1]) = (KanbanData[targetCardIndex + 1], KanbanData[targetCardIndex]); } -} \ No newline at end of file +} diff --git a/README.md b/README.md index de29881..b576220 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,119 @@ -![](https://img.shields.io/endpoint?url=https://codecentral.devexpress.com/api/v1/VersionRange/1089539411/25.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/T1312597) [![](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) -# Product/Platform - Task +# Blazor - Implement a Kanban Board Component with the DevExpress Blazor Grid -This is the repository template for creating new examples. Describe the solved task here. +This example implements a Kanban Board component (`DxKanban`) using the [DevExpress Blazor Grid](https://docs.devexpress.com/Blazor/403143/components/grid). The Kanban Board UI component includes the following features/capabilities: -Put a screenshot that illustrates the result here. +* Organizes cards across columns +* Allows users to reorder cards/columns via drag & drop -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. +![Blazor Kanban Board UI Component](images/blazor-kanban-board.gif) + +## Add a Kanban Board to Your Application + +Follow the steps below to add a Kanban Board component to your DevExpress-powered Blazor application: + +1. Copy the [DxKanban](./CS/BlazorKanban/Components/DxKanban) folder to your application's *Components* folder. + +2. Copy the [kanban.css](./CS/BlazorKanban/wwwroot/css/kanban.css) stylesheet to the *wwwroot/css* folder. Register the stylesheet in the *App.razor* file: + + ```razor + @DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties => { + properties.AddFilePaths("css/kanban.css"); + // ... + })) + ``` + +3. Copy and register the [card-styles.css](./CS/BlazorKanban/wwwroot/css/card-styles.css) file to recreate card appearance (defined in `CardTemplate`). Skip this step if you plan to customize card layout and styling. + + ```razor + @DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties => { + properties.AddFilePaths("css/card-styles.css"); + // ... + })) + ``` + +4. Register the `BlazorKanban.Components.DxKanban` namespace in the *Components/Imports.razor* file. + +5. Open/create a Razor page and enable interactivity. + +6. Add the Kanban Board component (`DxKanban`) to the page and configure component settings (refer to the next section). + +## DxKanban API Members + +### Properties + +- **Data** +Specifies an [IEnumerable](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1) object that supplies Kanban Board data. + +- **CardTemplate** +Defines card appearance. + +- **ColumnNameFieldName** +Specifies a data field that identifies the card target column. + +- **Columns** +Allows you to add Kanban Board columns (`DxKanbanColumn`). For each column, assign an identifier to the `ColumnName` property. This identifier must match a `ColumnNameFieldName` field value. + +- **CssClass** +Allows you to customize component appearance using CSS. + +### Events + +- **CardDropped** + + Fires on a card drop. In the event handler, update the data source: insert the card at the drop position and remove it from the initial position. + + For simplicity, this event uses [GridItemsDroppedEventArgs](https://docs.devexpress.com/Blazor/DevExpress.Blazor.GridItemsDroppedEventArgs). + +## Implementation Details + +Internally, the Kanban Board component is a [DevExpress Blazor Grid](https://docs.devexpress.com/Blazor/403143/components/grid) that displays nested Grids within columns. Each card is a nested Grid row. + +Main classes include: + +* [DxKanban](./CS/BlazorKanban/Components/DxKanban/DxKanban.razor) + + Creates a root-level Grid component. The Grid is bound to a fake single-row data source with multiple reorderable columns (`DxKanbanColumn`). The component uses [CascadingValue](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/cascading-values-and-parameters#cascadingvalue-component) to pass Kanban Board settings to columns. + + To allow users to drag cards without a drag handle, the component executes JS code in [AfterRenderAsync](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle#after-component-render-onafterrenderasync). + +* [DxKanbanColumn](./CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor) + + Creates a [DxGridDataColumn](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxGridDataColumn) with one data cell. The data cell contains a nested Grid with cards (`DxKanbanColumnGrid`). + +* [DxKanbanColumnGrid](./CS/BlazorKanban/Components/DxKanban/DxKanbanColumnGrid.razor) + + A nested Grid component that obtains Kanban Board data, filters it by column name (`DxKanban.ColumnNameFieldName`), and displays the resulting collection as cards. [DragHintTextTemplate](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxGrid.DragHintTextTemplate) and [CellDisplayTemplate](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxGridDataColumn.CellDisplayTemplate) specify card appearance. + + The Grid processes card drop actions using the `DxKanban.CardDropped` event handler. ## Files to Review -- link.cs (VB: link.vb) -- link.js -- ... +- [Index.razor](./CS/BlazorKanban/Components/Pages/Index.razor) +- [DxKanban.razor](./CS/BlazorKanban/Components/DxKanban/DxKanban.razor) + - [DxKanban.razor.cs](./CS/BlazorKanban/Components/DxKanban/DxKanban.razor.cs) + - [DxKanban.razor.js](./CS/BlazorKanban/Components/DxKanban/DxKanban.razor.js) +- [DxKanbanColumn.razor](./CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor) + - [DxKanbanColumn.razor.cs](./CS/BlazorKanban/Components/DxKanban/DxKanbanColumn.razor.cs) +- [DxKanbanColumnGrid.razor](./CS/BlazorKanban/Components/DxKanban/DxKanbanColumnGrid.razor) + - [DxKanbanColumnGrid.razor.cs](./CS/BlazorKanban/Components/DxKanban/DxKanbanColumnGrid.razor.cs) +- [KanbanModel.cs](./CS/BlazorKanban/Data/KanbanModel.cs) +- [kanban.css](./CS/BlazorKanban/wwwroot/css/kanban.css) +- [card-styles.css](./CS/BlazorKanban/wwwroot/css/card-styles.css) ## Documentation -- link -- link -- ... +- [DevExpress Blazor Components](https://docs.devexpress.com/Blazor/400725/blazor-components) +- [Drag and Drop Rows in Blazor Grid](https://docs.devexpress.com/Blazor/405231/components/grid/drag-and-drop-rows) ## More Examples -- link -- link -- ... +- [Blazor - Use DevExtreme Diagram in Blazor Applications](https://github.com/DevExpress-Examples/blazor-use-devextreme-diagram) + ## Does this example address your development requirements/objectives? diff --git a/images/blazor-kanban-board.gif b/images/blazor-kanban-board.gif new file mode 100644 index 0000000..7fdc549 Binary files /dev/null and b/images/blazor-kanban-board.gif differ