mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-04-05 22:01:04 +00:00
1872 lines
60 KiB
C#
1872 lines
60 KiB
C#
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
using Microsoft.JSInterop;
|
|
using Radzen;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Linq.Dynamic.Core;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Radzen.Blazor
|
|
{
|
|
/// <summary>
|
|
/// Displays tasks in a split view: hierarchical rows on the left and a timeline with task bars on the right.
|
|
/// </summary>
|
|
/// <typeparam name="TItem">Task item type.</typeparam>
|
|
#if NET6_0_OR_GREATER
|
|
[CascadingTypeParameter(nameof(TItem))]
|
|
#endif
|
|
public partial class RadzenGantt<TItem> where TItem : notnull
|
|
{
|
|
private RadzenScheduler<TItem>? scheduler;
|
|
private RadzenGanttDayView<TItem>? dayView;
|
|
private RadzenGanttWeekView<TItem>? weekView;
|
|
private RadzenGanttMonthView<TItem>? monthView;
|
|
private RadzenGanttYearView<TItem>? yearView;
|
|
private GanttZoomLevel? pendingZoomLevel;
|
|
private bool scrollToFirstEvent = true;
|
|
|
|
#region Data & Columns
|
|
|
|
/// <summary>
|
|
/// Task items.
|
|
/// </summary>
|
|
[Parameter]
|
|
public IEnumerable<TItem>? Data { get; set; }
|
|
|
|
/// <summary>
|
|
/// Optional task dependencies to draw connecting lines using object references.
|
|
/// For database scenarios, prefer <see cref="DependencyData"/> with property-name-based binding.
|
|
/// </summary>
|
|
[Parameter]
|
|
public IEnumerable<GanttDependency<TItem>>? Dependencies { get; set; }
|
|
|
|
/// <summary>
|
|
/// Collection of dependency items (any POCO with predecessor/successor ID properties).
|
|
/// Use together with <see cref="DependencyFromProperty"/>, <see cref="DependencyToProperty"/>,
|
|
/// and optionally <see cref="DependencyTypeProperty"/>.
|
|
/// When set, takes priority over <see cref="Dependencies"/>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public IEnumerable<object>? DependencyData { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name on <see cref="DependencyData"/> items that holds the predecessor task ID
|
|
/// (must match values of <see cref="IdProperty"/> on the task items).
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? DependencyFromProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name on <see cref="DependencyData"/> items that holds the successor task ID
|
|
/// (must match values of <see cref="IdProperty"/> on the task items).
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? DependencyToProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Optional property name on <see cref="DependencyData"/> items that holds the dependency type.
|
|
/// When not set, all dependencies default to <see cref="GanttDependencyType.FinishToStart"/>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? DependencyTypeProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Optional columns definition for the left pane.
|
|
/// </summary>
|
|
[Parameter]
|
|
public RenderFragment? Columns { get; set; }
|
|
|
|
#endregion
|
|
|
|
Func<TItem, object>? idGetter;
|
|
Func<TItem, object?>? parentIdGetter;
|
|
Func<TItem, object>? textGetter;
|
|
Func<TItem, dynamic>? progressGetter;
|
|
/// <inheritdoc />
|
|
protected override void OnParametersSet()
|
|
{
|
|
if (pendingZoomLevel != ZoomLevel)
|
|
{
|
|
pendingZoomLevel = ZoomLevel;
|
|
}
|
|
|
|
InvalidateTimelineCache();
|
|
base.OnParametersSet();
|
|
|
|
if (idGetter == null && !string.IsNullOrEmpty(IdProperty))
|
|
{
|
|
idGetter = PropertyAccess.Getter<TItem, object>(IdProperty);
|
|
}
|
|
|
|
if (parentIdGetter == null && !string.IsNullOrEmpty(ParentIdProperty))
|
|
{
|
|
parentIdGetter = PropertyAccess.Getter<TItem, object>(ParentIdProperty);
|
|
}
|
|
|
|
if (textGetter == null && !string.IsNullOrEmpty(TextProperty))
|
|
{
|
|
textGetter = PropertyAccess.Getter<TItem, object>(TextProperty);
|
|
}
|
|
|
|
if (progressGetter == null && !string.IsNullOrEmpty(ProgressProperty))
|
|
{
|
|
progressGetter = PropertyAccess.Getter<TItem, object>(ProgressProperty);
|
|
}
|
|
}
|
|
/// <inheritdoc />
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
await base.OnAfterRenderAsync(firstRender);
|
|
|
|
if (Visible && JSRuntime != null)
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("Radzen.ganttSyncScroll", GetId());
|
|
}
|
|
|
|
if (pendingZoomLevel.HasValue && scheduler != null)
|
|
{
|
|
var view = GetViewForZoom(pendingZoomLevel.Value);
|
|
if (view != null)
|
|
{
|
|
await scheduler.SelectView(view);
|
|
}
|
|
pendingZoomLevel = null;
|
|
InvalidateTimelineCache();
|
|
scrollToFirstEvent = true;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
if (scrollToFirstEvent && Visible && JSRuntime != null)
|
|
{
|
|
var scrolled = await JSRuntime.InvokeAsync<bool>("Radzen.ganttScrollToFirstEvent", GetId());
|
|
if (scrolled)
|
|
{
|
|
scrollToFirstEvent = false;
|
|
}
|
|
}
|
|
}
|
|
/// <inheritdoc />
|
|
public override void Dispose()
|
|
{
|
|
if (IsJSRuntimeAvailable && JSRuntime != null)
|
|
{
|
|
JSRuntime.InvokeVoid("Radzen.ganttSyncScrollDispose", GetId()!);
|
|
}
|
|
|
|
base.Dispose();
|
|
}
|
|
|
|
#region Gantt-specific properties
|
|
|
|
/// <summary>
|
|
/// Property name used as unique task id.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? IdProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name used as parent task id.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? ParentIdProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name used for task title (shown in the first column when no template is provided).
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? TextProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name used for task start date.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? StartProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name used for task end date.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? EndProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name used for task progress (0..100). Optional.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? ProgressProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Optional explicit timeline start.
|
|
/// </summary>
|
|
[Parameter]
|
|
public DateTime? ViewStart { get; set; }
|
|
|
|
/// <summary>
|
|
/// Optional explicit timeline end.
|
|
/// </summary>
|
|
[Parameter]
|
|
public DateTime? ViewEnd { get; set; }
|
|
|
|
/// <summary>
|
|
/// Timeline zoom.
|
|
/// </summary>
|
|
[Parameter]
|
|
public GanttZoomLevel ZoomLevel { get; set; } = GanttZoomLevel.Week;
|
|
|
|
/// <summary>
|
|
/// Width of the left pane (CSS length).
|
|
/// </summary>
|
|
[Parameter]
|
|
public string LeftPaneWidth { get; set; } = "50%";
|
|
|
|
/// <summary>
|
|
/// Shows the unified Gantt navigation header.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowNavigation { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Row height in pixels.
|
|
/// </summary>
|
|
[Parameter]
|
|
public int RowHeightPx { get; set; } = 36;
|
|
|
|
/// <summary>
|
|
/// Day cell width in pixels.
|
|
/// </summary>
|
|
[Parameter]
|
|
public int DayWidthPx { get; set; } = 80;
|
|
|
|
/// <summary>
|
|
/// Number of weeks to render in Week view.
|
|
/// </summary>
|
|
[Parameter]
|
|
public int WeeksInView { get; set; } = 4;
|
|
|
|
/// <summary>
|
|
/// Date format for header cells.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string HeaderDateFormat { get; set; } = "dd MMM";
|
|
|
|
/// <summary>
|
|
/// Raised when a task bar is clicked.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<TItem> TaskClick { get; set; }
|
|
|
|
/// <summary>
|
|
/// Raised when the mouse enters a task bar. Commonly used to show a tooltip.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<GanttTaskMouseEventArgs<TItem>> TaskMouseEnter { get; set; }
|
|
|
|
/// <summary>
|
|
/// Raised when the mouse leaves a task bar. Commonly used to close a tooltip.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<GanttTaskMouseEventArgs<TItem>> TaskMouseLeave { get; set; }
|
|
|
|
/// <summary>
|
|
/// Raised when a task bar is dragged to a new position on the timeline.
|
|
/// The consumer should update the data item's start and end dates.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<GanttTaskMovedEventArgs<TItem>> TaskMove { get; set; }
|
|
|
|
/// <summary>
|
|
/// Raised when a task bar edge is dragged to resize the task.
|
|
/// The consumer should update the data item's start and/or end date.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<GanttTaskMovedEventArgs<TItem>> TaskResize { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name for the baseline (planned) start date. When set together with <see cref="BaselineEndProperty"/>,
|
|
/// a secondary bar is rendered behind the actual bar showing planned vs actual.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? BaselineStartProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Property name for the baseline (planned) end date.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? BaselineEndProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// When <c>true</c>, highlights the critical path — the longest chain of dependent tasks
|
|
/// that determines the project end date. Requires <see cref="Dependencies"/> to be set.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowCriticalPath { get; set; }
|
|
|
|
/// <summary>
|
|
/// When <c>true</c>, draws a vertical line on the timeline at the current date/time.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowTodayLine { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// When <c>true</c>, shades non-working days (Saturday and Sunday by default) on the timeline.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowWeekends { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// The days of the week considered non-working. Used when <see cref="ShowWeekends"/> is <c>true</c>.
|
|
/// Defaults to Saturday and Sunday.
|
|
/// </summary>
|
|
[Parameter]
|
|
public IEnumerable<DayOfWeek> NonWorkingDays { get; set; } = new[] { DayOfWeek.Saturday, DayOfWeek.Sunday };
|
|
|
|
/// <summary>
|
|
/// Optional vertical date markers rendered on the timeline (e.g. deadlines, milestones, releases).
|
|
/// </summary>
|
|
[Parameter]
|
|
public IEnumerable<GanttMarker>? Markers { get; set; }
|
|
|
|
/// <summary>
|
|
/// Callback to customize the appearance of each task bar. Set <see cref="GanttBarRenderEventArgs{TItem}.CssClass"/>
|
|
/// or <see cref="GanttBarRenderEventArgs{TItem}.Attributes"/> to change colors or styles per task.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Action<GanttBarRenderEventArgs<TItem>>? TaskRender { get; set; }
|
|
|
|
/// <summary>
|
|
/// Custom template for the content rendered inside each task bar. When set, replaces the
|
|
/// default progress bar and label. Receives the task data item as context.
|
|
/// </summary>
|
|
[Parameter]
|
|
public RenderFragment<TItem>? TaskTemplate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Text for the "Today" navigation button. Default is <c>"Today"</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string TodayText { get; set; } = "Today";
|
|
|
|
/// <summary>
|
|
/// Tooltip for the "Previous" navigation button. Default is <c>"Previous"</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string PrevText { get; set; } = "Previous";
|
|
|
|
/// <summary>
|
|
/// Tooltip for the "Next" navigation button. Default is <c>"Next"</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string NextText { get; set; } = "Next";
|
|
|
|
/// <summary>
|
|
/// Tooltip for the "Zoom to fit" navigation button. Default is <c>"Zoom to fit"</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string ZoomToFitText { get; set; } = "Zoom to fit";
|
|
|
|
#endregion
|
|
|
|
#region DataGrid Proxied Properties
|
|
|
|
/// <summary>
|
|
/// Enables sorting in the left pane grid. Default is <c>true</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowSorting { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Enables multi-column sorting. Default is <c>false</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowMultiColumnSorting { get; set; }
|
|
|
|
/// <summary>
|
|
/// Shows multi-column sorting index. Default is <c>false</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowMultiColumnSortingIndex { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether to go to the first page on sort. Default is <c>false</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool GotoFirstPageOnSort { get; set; }
|
|
|
|
/// <summary>
|
|
/// Enables filtering in the left pane grid. Default is <c>true</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowFiltering { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the filter mode.
|
|
/// </summary>
|
|
[Parameter]
|
|
public FilterMode FilterMode { get; set; } = FilterMode.Advanced;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the logical filter operator.
|
|
/// </summary>
|
|
[Parameter]
|
|
public LogicalFilterOperator LogicalFilterOperator { get; set; } = LogicalFilterOperator.And;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the filter popup render mode.
|
|
/// </summary>
|
|
[Parameter]
|
|
public PopupRenderMode FilterPopupRenderMode { get; set; } = PopupRenderMode.Initial;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the filter case sensitivity.
|
|
/// </summary>
|
|
[Parameter]
|
|
public FilterCaseSensitivity FilterCaseSensitivity { get; set; } = FilterCaseSensitivity.Default;
|
|
|
|
/// <summary>
|
|
/// Enables paging in the left pane grid. Default is <c>false</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowPaging { get; set; }
|
|
|
|
/// <summary>
|
|
/// Page size when <see cref="AllowPaging"/> is enabled.
|
|
/// </summary>
|
|
[Parameter]
|
|
public int PageSize { get; set; } = 50;
|
|
|
|
/// <summary>
|
|
/// Enables column resizing. Default is <c>false</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowColumnResize { get; set; }
|
|
|
|
/// <summary>
|
|
/// Enables column reordering. Default is <c>false</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowColumnReorder { get; set; }
|
|
|
|
/// <summary>
|
|
/// Enables column picking. Default is <c>false</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowColumnPicking { get; set; }
|
|
|
|
/// <summary>
|
|
/// Enables row virtualization. Default is <c>false</c>.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowVirtualization { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the virtualization overscan count.
|
|
/// </summary>
|
|
[Parameter]
|
|
public int VirtualizationOverscanCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the grid lines.
|
|
/// </summary>
|
|
[Parameter]
|
|
public DataGridGridLines GridLines { get; set; } = DataGridGridLines.Default;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the density.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Density Density { get; set; } = Density.Default;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether empty message is shown.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowEmptyMessage { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the empty text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string EmptyText { get; set; } = "No records to display.";
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether loading indicator is shown.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool IsLoading { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether grid is responsive.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool Responsive { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether header is shown.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowHeader { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the edit mode.
|
|
/// </summary>
|
|
[Parameter]
|
|
public DataGridEditMode EditMode { get; set; } = DataGridEditMode.Multiple;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the expand mode for hierarchical data.
|
|
/// </summary>
|
|
[Parameter]
|
|
public DataGridExpandMode ExpandMode { get; set; } = DataGridExpandMode.Multiple;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether alternating rows are shown.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowAlternatingRows { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether row selection is allowed on row click.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowRowSelectOnRowClick { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether to show cell data as tooltip.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowCellDataAsTooltip { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether to show column title as tooltip.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool ShowColumnTitleAsTooltip { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether filter date input is allowed.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool AllowFilterDateInput { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region DataGrid Proxied Filter Text
|
|
|
|
/// <summary>
|
|
/// Filter text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string FilterText { get; set; } = "Filter";
|
|
|
|
/// <summary>
|
|
/// And operator text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string AndOperatorText { get; set; } = "And";
|
|
|
|
/// <summary>
|
|
/// Or operator text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string OrOperatorText { get; set; } = "Or";
|
|
|
|
/// <summary>
|
|
/// Apply filter text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string ApplyFilterText { get; set; } = "Apply";
|
|
|
|
/// <summary>
|
|
/// Clear filter text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string ClearFilterText { get; set; } = "Clear";
|
|
|
|
/// <summary>
|
|
/// Equals text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string EqualsText { get; set; } = "Equals";
|
|
|
|
/// <summary>
|
|
/// Not equals text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string NotEqualsText { get; set; } = "Not equals";
|
|
|
|
/// <summary>
|
|
/// Less than text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string LessThanText { get; set; } = "Less than";
|
|
|
|
/// <summary>
|
|
/// Less than or equals text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string LessThanOrEqualsText { get; set; } = "Less than or equals";
|
|
|
|
/// <summary>
|
|
/// Greater than text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string GreaterThanText { get; set; } = "Greater than";
|
|
|
|
/// <summary>
|
|
/// Greater than or equals text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string GreaterThanOrEqualsText { get; set; } = "Greater than or equals";
|
|
|
|
/// <summary>
|
|
/// Contains text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string ContainsText { get; set; } = "Contains";
|
|
|
|
/// <summary>
|
|
/// Does not contain text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string DoesNotContainText { get; set; } = "Does not contain";
|
|
|
|
/// <summary>
|
|
/// Starts with text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string StartsWithText { get; set; } = "Starts with";
|
|
|
|
/// <summary>
|
|
/// Ends with text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string EndsWithText { get; set; } = "Ends with";
|
|
|
|
/// <summary>
|
|
/// Is null text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string IsNullText { get; set; } = "Is null";
|
|
|
|
/// <summary>
|
|
/// Is not null text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string IsNotNullText { get; set; } = "Is not null";
|
|
|
|
/// <summary>
|
|
/// Is empty text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string IsEmptyText { get; set; } = "Is empty";
|
|
|
|
/// <summary>
|
|
/// Is not empty text.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string IsNotEmptyText { get; set; } = "Is not empty";
|
|
|
|
#endregion
|
|
|
|
#region DataGrid Proxied Events
|
|
|
|
/// <summary>
|
|
/// Column sort callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridColumnSortEventArgs<TItem>> Sort { get; set; }
|
|
|
|
/// <summary>
|
|
/// Column filter callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridColumnFilterEventArgs<TItem>> Filter { get; set; }
|
|
|
|
/// <summary>
|
|
/// Column filter cleared callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridColumnFilterEventArgs<TItem>> FilterCleared { get; set; }
|
|
|
|
/// <summary>
|
|
/// Column resized callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridColumnResizedEventArgs<TItem>> ColumnResized { get; set; }
|
|
|
|
/// <summary>
|
|
/// Column reordering callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridColumnReorderingEventArgs<TItem>> ColumnReordering { get; set; }
|
|
|
|
/// <summary>
|
|
/// Column reordered callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridColumnReorderedEventArgs<TItem>> ColumnReordered { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row click callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridRowMouseEventArgs<TItem>> RowClick { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row double click callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridRowMouseEventArgs<TItem>> RowDoubleClick { get; set; }
|
|
|
|
/// <summary>
|
|
/// Cell click callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridCellMouseEventArgs<TItem>> CellClick { get; set; }
|
|
|
|
/// <summary>
|
|
/// Cell double click callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridCellMouseEventArgs<TItem>> CellDoubleClick { get; set; }
|
|
|
|
/// <summary>
|
|
/// Cell context menu callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridCellMouseEventArgs<TItem>> CellContextMenu { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row select callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<TItem> RowSelect { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row deselect callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<TItem> RowDeselect { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row expand callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<TItem> RowExpand { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row collapse callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<TItem> RowCollapse { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row edit callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<TItem> RowEdit { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row update callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<TItem> RowUpdate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Row create callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<TItem> RowCreate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Key down callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<KeyboardEventArgs> KeyDown { get; set; }
|
|
|
|
/// <summary>
|
|
/// Page size changed callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<int> PageSizeChanged { get; set; }
|
|
|
|
/// <summary>
|
|
/// Picked columns changed callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridPickedColumnsChangedEventArgs<TItem>> PickedColumnsChanged { get; set; }
|
|
|
|
/// <summary>
|
|
/// Load settings callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Action<DataGridLoadSettingsEventArgs>? LoadSettings { get; set; }
|
|
|
|
/// <summary>
|
|
/// Settings changed callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<DataGridSettings> SettingsChanged { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region DataGrid Proxied Action Parameters
|
|
|
|
/// <summary>
|
|
/// Row render callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Action<RowRenderEventArgs<TItem>>? RowRender { get; set; }
|
|
|
|
/// <summary>
|
|
/// Cell render callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Action<DataGridCellRenderEventArgs<TItem>>? CellRender { get; set; }
|
|
|
|
/// <summary>
|
|
/// Header cell render callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Action<DataGridCellRenderEventArgs<TItem>>? HeaderCellRender { get; set; }
|
|
|
|
/// <summary>
|
|
/// Footer cell render callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Action<DataGridCellRenderEventArgs<TItem>>? FooterCellRender { get; set; }
|
|
|
|
/// <summary>
|
|
/// Render callback.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Action<DataGridRenderEventArgs<TItem>>? Render { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Public methods
|
|
|
|
/// <summary>
|
|
/// Returns whether the underlying grid has a valid edit state.
|
|
/// </summary>
|
|
public bool IsValid => grid?.IsValid ?? true;
|
|
|
|
/// <summary>
|
|
/// Puts the specified item in edit mode.
|
|
/// </summary>
|
|
/// <param name="item">The item to edit.</param>
|
|
public async System.Threading.Tasks.Task EditRow(TItem item)
|
|
{
|
|
if (grid != null)
|
|
{
|
|
await grid.EditRow(item);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the specified item and exits edit mode.
|
|
/// </summary>
|
|
/// <param name="item">The item to update.</param>
|
|
public async System.Threading.Tasks.Task UpdateRow(TItem item)
|
|
{
|
|
if (grid != null)
|
|
{
|
|
await grid.UpdateRow(item);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels edit mode for the specified item.
|
|
/// </summary>
|
|
/// <param name="item">The item to cancel editing for.</param>
|
|
public void CancelEditRow(TItem item)
|
|
{
|
|
grid?.CancelEditRow(item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a new item into the grid in edit mode.
|
|
/// </summary>
|
|
/// <param name="item">The item to insert.</param>
|
|
public async System.Threading.Tasks.Task InsertRow(TItem item)
|
|
{
|
|
if (grid != null)
|
|
{
|
|
await grid.InsertRow(item);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a new item after the specified row in edit mode.
|
|
/// </summary>
|
|
/// <param name="item">The new item to insert.</param>
|
|
/// <param name="afterItem">The item after which to insert.</param>
|
|
public async System.Threading.Tasks.Task InsertAfterRow(TItem item, TItem afterItem)
|
|
{
|
|
if (grid != null)
|
|
{
|
|
await grid.InsertAfterRow(item, afterItem);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reloads the underlying grid data.
|
|
/// </summary>
|
|
public async System.Threading.Tasks.Task Reload()
|
|
{
|
|
if (grid != null)
|
|
{
|
|
await grid.Reload();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal state
|
|
|
|
internal readonly List<RadzenGanttColumn<TItem>> GanttColumns = new();
|
|
|
|
internal void AddColumn(RadzenGanttColumn<TItem> column)
|
|
{
|
|
if (!GanttColumns.Contains(column))
|
|
{
|
|
GanttColumns.Add(column);
|
|
}
|
|
}
|
|
|
|
internal void RemoveColumn(RadzenGanttColumn<TItem> column)
|
|
{
|
|
GanttColumns.Remove(column);
|
|
}
|
|
|
|
internal bool IsTreeColumn(RadzenGanttColumn<TItem> column)
|
|
{
|
|
// First declared RadzenGanttColumn is the tree column.
|
|
return GanttColumns.Count > 0 && ReferenceEquals(GanttColumns[0], column);
|
|
}
|
|
|
|
internal sealed record GanttRow(TItem Item, int Level, bool HasChildren, bool IsExpanded);
|
|
|
|
internal RadzenDataGrid<TItem>? grid;
|
|
|
|
private IReadOnlyList<DateTime>? cachedTimelineDays;
|
|
private int? cachedTimelineRowCount;
|
|
private IReadOnlyDictionary<object, int>? cachedTimelineRowIndex;
|
|
private DateTime? cachedSchedulerDate;
|
|
private Func<TItem, object>? cachedStartGetter;
|
|
private Func<TItem, object>? cachedEndGetter;
|
|
|
|
private void InvalidateTimelineCache()
|
|
{
|
|
cachedTimelineDays = null;
|
|
cachedTimelineRowCount = null;
|
|
cachedTimelineRowIndex = null;
|
|
cachedSchedulerDate = null;
|
|
}
|
|
|
|
private Func<TItem, object>? GetStartGetter()
|
|
{
|
|
if (cachedStartGetter == null && !string.IsNullOrWhiteSpace(StartProperty))
|
|
{
|
|
cachedStartGetter = PropertyAccess.Getter<TItem, object>(StartProperty);
|
|
}
|
|
return cachedStartGetter;
|
|
}
|
|
|
|
private Func<TItem, object>? GetEndGetter()
|
|
{
|
|
if (cachedEndGetter == null && !string.IsNullOrWhiteSpace(EndProperty))
|
|
{
|
|
cachedEndGetter = PropertyAccess.Getter<TItem, object>(EndProperty);
|
|
}
|
|
return cachedEndGetter;
|
|
}
|
|
|
|
internal IReadOnlyList<DateTime> TimelineDays => cachedTimelineDays ??= BuildTimelineDays();
|
|
|
|
internal int TimelineWidthPx => TimelineDays.Count * DayWidthPx;
|
|
|
|
internal int TimelineRowCount => cachedTimelineRowCount ??= (TimelineRowIndexByItem.Count > 0 ? TimelineRowIndexByItem.Count : (grid?.View?.Count() ?? gridCount));
|
|
|
|
internal IReadOnlyDictionary<object, int> TimelineRowIndexByItem => cachedTimelineRowIndex ??= BuildTimelineRowIndex();
|
|
|
|
internal DateTime SchedulerDate => cachedSchedulerDate ??= GetSchedulerDate();
|
|
|
|
internal IEnumerable<GanttDependency<TItem>>? TimelineDependencies => ResolveDependencies();
|
|
|
|
internal string? TimelineBaselineStartProperty => BaselineStartProperty;
|
|
internal string? TimelineBaselineEndProperty => BaselineEndProperty;
|
|
internal bool TimelineShowTodayLine => ShowTodayLine;
|
|
internal bool TimelineShowWeekends => ShowWeekends;
|
|
internal IEnumerable<DayOfWeek> TimelineNonWorkingDays => NonWorkingDays;
|
|
internal IEnumerable<GanttMarker>? TimelineMarkers => Markers;
|
|
internal Action<GanttBarRenderEventArgs<TItem>>? TimelineTaskRender => TaskRender;
|
|
internal RenderFragment<TItem>? TimelineTaskTemplate => TaskTemplate;
|
|
|
|
internal string RootStyle
|
|
{
|
|
get
|
|
{
|
|
var rowHeight = RowHeightPx.ToString(CultureInfo.InvariantCulture);
|
|
var customVar = $"--rz-gantt-row-height: {rowHeight}px;";
|
|
if (string.IsNullOrWhiteSpace(Style))
|
|
{
|
|
return customVar;
|
|
}
|
|
|
|
return $"{Style}; {customVar}";
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <inheritdoc />
|
|
/// <inheritdoc />
|
|
protected override async Task AddContextMenu()
|
|
{
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override string GetComponentCssClass() => "rz-gantt";
|
|
|
|
#region Timeline Building
|
|
|
|
private IReadOnlyList<DateTime> BuildTimelineDays()
|
|
{
|
|
var data = pagedData?.ToList() ?? new List<TItem>();
|
|
var sg = GetStartGetter();
|
|
var eg = GetEndGetter();
|
|
if (data.Count == 0 || sg == null || eg == null)
|
|
{
|
|
return Array.Empty<DateTime>();
|
|
}
|
|
|
|
DateTime? min = ViewStart;
|
|
DateTime? max = ViewEnd;
|
|
|
|
if (min == null || max == null)
|
|
{
|
|
foreach (var item in data)
|
|
{
|
|
var s = ToDateTime(sg(item));
|
|
var e = ToDateTime(eg(item));
|
|
if (s.HasValue)
|
|
{
|
|
min = min.HasValue ? (s.Value < min.Value ? s.Value : min.Value) : s.Value;
|
|
}
|
|
if (e.HasValue)
|
|
{
|
|
max = max.HasValue ? (e.Value > max.Value ? e.Value : max.Value) : e.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!min.HasValue || !max.HasValue)
|
|
{
|
|
return Array.Empty<DateTime>();
|
|
}
|
|
|
|
var start = min.Value.Date;
|
|
var end = max.Value.Date;
|
|
if (end < start) (start, end) = (end, start);
|
|
|
|
var pad = ZoomLevel switch
|
|
{
|
|
GanttZoomLevel.Day => 2,
|
|
GanttZoomLevel.Week => 7,
|
|
GanttZoomLevel.Month => 14,
|
|
GanttZoomLevel.Year => 30,
|
|
_ => 14
|
|
};
|
|
start = start.AddDays(-pad);
|
|
end = end.AddDays(pad);
|
|
|
|
var days = new List<DateTime>();
|
|
for (var d = start; d <= end; d = d.AddDays(1))
|
|
{
|
|
days.Add(d);
|
|
}
|
|
return days;
|
|
}
|
|
|
|
private IReadOnlyDictionary<object, int> BuildTimelineRowIndex()
|
|
{
|
|
var map = new Dictionary<object, int>();
|
|
var index = 0;
|
|
foreach (var item in grid?.View ?? Enumerable.Empty<TItem>())
|
|
{
|
|
if (item != null)
|
|
{
|
|
map[item] = index;
|
|
}
|
|
index++;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
private DateTime GetSchedulerDate()
|
|
{
|
|
if (ViewStart.HasValue)
|
|
{
|
|
return ViewStart.Value.Date;
|
|
}
|
|
|
|
var sg = GetStartGetter();
|
|
if (Data == null || sg == null)
|
|
{
|
|
return DateTime.Today;
|
|
}
|
|
|
|
DateTime? min = null;
|
|
foreach (var item in pagedData ?? Enumerable.Empty<TItem>())
|
|
{
|
|
var start = ToDateTime(sg(item));
|
|
if (start.HasValue)
|
|
{
|
|
min = min.HasValue ? (start.Value < min.Value ? start.Value : min.Value) : start.Value;
|
|
}
|
|
}
|
|
|
|
return (min ?? DateTime.Today).Date;
|
|
}
|
|
|
|
private ISchedulerView? GetViewForZoom(GanttZoomLevel zoomLevel)
|
|
{
|
|
return zoomLevel switch
|
|
{
|
|
GanttZoomLevel.Day => dayView,
|
|
GanttZoomLevel.Month => monthView,
|
|
GanttZoomLevel.Year => yearView,
|
|
_ => weekView
|
|
};
|
|
}
|
|
|
|
private static DateTime? ToDateTime(object? value)
|
|
{
|
|
if (value == null) return null;
|
|
if (value is DateTime dt) return dt;
|
|
if (value is DateTimeOffset dto) return dto.DateTime;
|
|
if (value is string s && DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Dependencies
|
|
|
|
private IEnumerable<GanttDependency<TItem>> ResolveDependencies()
|
|
{
|
|
if (DependencyData != null && !string.IsNullOrEmpty(DependencyFromProperty) && !string.IsNullOrEmpty(DependencyToProperty))
|
|
{
|
|
return ResolveDependencyData();
|
|
}
|
|
|
|
return Dependencies ?? BuildDefaultDependencies();
|
|
}
|
|
|
|
private IEnumerable<GanttDependency<TItem>> ResolveDependencyData()
|
|
{
|
|
if (idGetter == null || Data == null || DependencyData == null)
|
|
{
|
|
return Enumerable.Empty<GanttDependency<TItem>>();
|
|
}
|
|
|
|
var taskById = new Dictionary<object, TItem>();
|
|
foreach (var item in Data)
|
|
{
|
|
var id = idGetter(item);
|
|
if (id != null)
|
|
{
|
|
taskById[id] = item;
|
|
}
|
|
}
|
|
|
|
Func<object, object>? fromGetter = null;
|
|
Func<object, object>? toGetter = null;
|
|
Func<object, object>? typeGetter = null;
|
|
|
|
var links = new List<GanttDependency<TItem>>();
|
|
foreach (var dep in DependencyData)
|
|
{
|
|
if (dep == null) continue;
|
|
|
|
fromGetter ??= PropertyAccess.Getter<object>(dep, DependencyFromProperty!);
|
|
toGetter ??= PropertyAccess.Getter<object>(dep, DependencyToProperty!);
|
|
if (!string.IsNullOrEmpty(DependencyTypeProperty))
|
|
{
|
|
typeGetter ??= PropertyAccess.Getter<object>(dep, DependencyTypeProperty);
|
|
}
|
|
|
|
var fromId = fromGetter(dep);
|
|
var toId = toGetter(dep);
|
|
if (fromId == null || toId == null) continue;
|
|
|
|
if (!taskById.TryGetValue(fromId, out var fromTask) || !taskById.TryGetValue(toId, out var toTask))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var type = GanttDependencyType.FinishToStart;
|
|
if (typeGetter != null)
|
|
{
|
|
var rawType = typeGetter(dep);
|
|
if (rawType is GanttDependencyType gdt)
|
|
{
|
|
type = gdt;
|
|
}
|
|
else if (rawType != null && Enum.TryParse<GanttDependencyType>(rawType.ToString(), out var parsed))
|
|
{
|
|
type = parsed;
|
|
}
|
|
}
|
|
|
|
links.Add(new GanttDependency<TItem> { From = fromTask, To = toTask, Type = type });
|
|
}
|
|
|
|
return links;
|
|
}
|
|
|
|
private IEnumerable<GanttDependency<TItem>> BuildDefaultDependencies()
|
|
{
|
|
if (idGetter == null || parentIdGetter == null || Data == null)
|
|
{
|
|
return Enumerable.Empty<GanttDependency<TItem>>();
|
|
}
|
|
|
|
var map = new Dictionary<object, TItem>();
|
|
foreach (var item in Data)
|
|
{
|
|
var id = idGetter(item);
|
|
if (id != null)
|
|
{
|
|
map[id] = item;
|
|
}
|
|
}
|
|
|
|
var links = new List<GanttDependency<TItem>>();
|
|
foreach (var item in Data)
|
|
{
|
|
var parentId = parentIdGetter(item);
|
|
if (parentId == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (map.TryGetValue(parentId, out var parent))
|
|
{
|
|
links.Add(new GanttDependency<TItem> { From = parent, To = item });
|
|
}
|
|
}
|
|
|
|
return links;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Critical Path
|
|
|
|
internal HashSet<object>? CriticalPathItems { get; private set; }
|
|
|
|
internal void ComputeCriticalPath(IEnumerable<AppointmentData>? appointments, IReadOnlyDictionary<object, int>? rowIndexByItem)
|
|
{
|
|
CriticalPathItems = null;
|
|
|
|
var startGet = GetStartGetter();
|
|
var endGet = GetEndGetter();
|
|
|
|
var resolvedDeps = ResolveDependencies();
|
|
if (!ShowCriticalPath || resolvedDeps == null || !resolvedDeps.Any() || appointments == null || startGet == null || endGet == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var items = new List<TItem>();
|
|
var itemSet = new HashSet<object>();
|
|
foreach (var a in appointments)
|
|
{
|
|
if (a.Data is TItem t)
|
|
{
|
|
items.Add(t);
|
|
itemSet.Add(t);
|
|
}
|
|
}
|
|
|
|
if (items.Count == 0) return;
|
|
|
|
var deps = resolvedDeps
|
|
.Where(d => d != null && itemSet.Contains(d.From) && itemSet.Contains(d.To))
|
|
.ToList();
|
|
|
|
var successors = new Dictionary<object, List<(TItem To, GanttDependencyType Type)>>();
|
|
var predecessors = new Dictionary<object, List<(TItem From, GanttDependencyType Type)>>();
|
|
foreach (var item in items)
|
|
{
|
|
successors[item] = new List<(TItem, GanttDependencyType)>();
|
|
predecessors[item] = new List<(TItem, GanttDependencyType)>();
|
|
}
|
|
foreach (var d in deps)
|
|
{
|
|
successors[d.From].Add((d.To, d.Type));
|
|
predecessors[d.To].Add((d.From, d.Type));
|
|
}
|
|
|
|
var es = new Dictionary<object, double>();
|
|
var ef = new Dictionary<object, double>();
|
|
var ls = new Dictionary<object, double>();
|
|
var lf = new Dictionary<object, double>();
|
|
var dur = new Dictionary<object, double>();
|
|
|
|
DateTime refDate = DateTime.MaxValue;
|
|
foreach (var t in items)
|
|
{
|
|
var s = ToDateTime(startGet(t));
|
|
if (s.HasValue && s.Value < refDate) refDate = s.Value;
|
|
}
|
|
|
|
foreach (var t in items)
|
|
{
|
|
var s = ToDateTime(startGet(t)) ?? refDate;
|
|
var e = ToDateTime(endGet(t)) ?? s;
|
|
dur[t] = Math.Max(0, (e - s).TotalDays);
|
|
es[t] = (s - refDate).TotalDays;
|
|
ef[t] = es[t] + dur[t];
|
|
}
|
|
|
|
// Forward pass
|
|
bool changed = true;
|
|
for (int iter = 0; iter < items.Count && changed; iter++)
|
|
{
|
|
changed = false;
|
|
foreach (var t in items)
|
|
{
|
|
foreach (var (from, type) in predecessors[t])
|
|
{
|
|
double constraint = type switch
|
|
{
|
|
GanttDependencyType.FinishToStart => ef[from],
|
|
GanttDependencyType.StartToStart => es[from],
|
|
GanttDependencyType.FinishToFinish => ef[from] - dur[t],
|
|
GanttDependencyType.StartToFinish => es[from] - dur[t],
|
|
_ => ef[from]
|
|
};
|
|
if (constraint > es[t])
|
|
{
|
|
es[t] = constraint;
|
|
ef[t] = es[t] + dur[t];
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
double projectEnd = items.Max(t => ef[t]);
|
|
|
|
// Backward pass
|
|
foreach (var t in items)
|
|
{
|
|
lf[t] = projectEnd;
|
|
ls[t] = lf[t] - dur[t];
|
|
}
|
|
|
|
changed = true;
|
|
for (int iter = 0; iter < items.Count && changed; iter++)
|
|
{
|
|
changed = false;
|
|
foreach (var t in items)
|
|
{
|
|
foreach (var (to, type) in successors[t])
|
|
{
|
|
double constraint = type switch
|
|
{
|
|
GanttDependencyType.FinishToStart => ls[to],
|
|
GanttDependencyType.StartToStart => ls[to] + dur[t],
|
|
GanttDependencyType.FinishToFinish => lf[to],
|
|
GanttDependencyType.StartToFinish => lf[to] + dur[t],
|
|
_ => ls[to]
|
|
};
|
|
if (type == GanttDependencyType.FinishToStart || type == GanttDependencyType.FinishToFinish)
|
|
{
|
|
if (constraint < lf[t])
|
|
{
|
|
lf[t] = constraint;
|
|
ls[t] = lf[t] - dur[t];
|
|
changed = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (constraint - dur[t] < ls[t])
|
|
{
|
|
ls[t] = constraint - dur[t];
|
|
lf[t] = ls[t] + dur[t];
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const double tolerance = 0.001;
|
|
CriticalPathItems = new HashSet<object>();
|
|
foreach (var t in items)
|
|
{
|
|
var slack = ls[t] - es[t];
|
|
if (Math.Abs(slack) < tolerance)
|
|
{
|
|
CriticalPathItems.Add(t);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Task Bar Rendering
|
|
|
|
internal RenderFragment RenderTaskBar(TItem item) => builder =>
|
|
{
|
|
var sg = GetStartGetter();
|
|
var eg = GetEndGetter();
|
|
if (sg == null || eg == null || TimelineDays.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var start = ToDateTime(sg(item));
|
|
var end = ToDateTime(eg(item));
|
|
|
|
if (!start.HasValue || !end.HasValue)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var rangeStart = TimelineDays[0];
|
|
var rangeEnd = TimelineDays[^1];
|
|
|
|
var s = start.Value.Date;
|
|
var e = end.Value.Date;
|
|
if (e < s) (s, e) = (e, s);
|
|
|
|
if (e < rangeStart || s > rangeEnd)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var clampedStart = s < rangeStart ? rangeStart : s;
|
|
var clampedEnd = e > rangeEnd ? rangeEnd : e;
|
|
|
|
var leftDays = (clampedStart - rangeStart).TotalDays;
|
|
var widthDays = Math.Max(1, (clampedEnd - clampedStart).TotalDays + 1);
|
|
|
|
var leftPx = (int)(leftDays * DayWidthPx);
|
|
var widthPx = (int)(widthDays * DayWidthPx);
|
|
|
|
var progress = progressGetter != null ? progressGetter(item) : null;
|
|
var progressWidth = progress != null ? Math.Max(0, Math.Min(100, progress)) : (double?)null;
|
|
|
|
builder.OpenElement(0, "div");
|
|
builder.AddAttribute(1, "class", "rz-gantt-bar");
|
|
builder.AddAttribute(2, "style", $"left:{leftPx}px;width:{widthPx}px;");
|
|
builder.AddAttribute(3, "onclick", EventCallback.Factory.Create(this, () => TaskClick.InvokeAsync(item)));
|
|
|
|
if (progressWidth != null)
|
|
{
|
|
builder.OpenElement(4, "div");
|
|
builder.AddAttribute(5, "class", "rz-gantt-bar-progress");
|
|
builder.AddAttribute(6, "style", $"width:{progressWidth.ToString(CultureInfo.InvariantCulture)}%;");
|
|
builder.CloseElement();
|
|
}
|
|
|
|
builder.OpenElement(7, "div");
|
|
builder.AddAttribute(8, "class", "rz-gantt-bar-label");
|
|
builder.AddContent(9, textGetter != null ? textGetter(item) : "");
|
|
builder.CloseElement();
|
|
|
|
builder.CloseElement();
|
|
};
|
|
|
|
#endregion
|
|
|
|
#region Navigation helpers
|
|
|
|
internal IEnumerable<ISchedulerView> GanttViews
|
|
{
|
|
get
|
|
{
|
|
if (dayView != null) yield return dayView;
|
|
if (weekView != null) yield return weekView;
|
|
if (monthView != null) yield return monthView;
|
|
if (yearView != null) yield return yearView;
|
|
}
|
|
}
|
|
|
|
internal string SchedulerTitle => scheduler?.SelectedView?.Title ?? string.Empty;
|
|
internal string SchedulerTodayText => TodayText;
|
|
internal string SchedulerPrevText => PrevText;
|
|
internal string SchedulerNextText => NextText;
|
|
internal bool IsNavigationDisabled => scheduler?.SelectedView == null;
|
|
|
|
internal string GetViewButtonCssClass(ISchedulerView view)
|
|
{
|
|
var isActive = scheduler?.SelectedView == view;
|
|
return isActive ? " rz-state-active" : string.Empty;
|
|
}
|
|
|
|
internal async Task OnGanttChangeView(ISchedulerView view)
|
|
{
|
|
if (scheduler == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InvalidateTimelineCache();
|
|
scrollToFirstEvent = true;
|
|
await scheduler.SelectView(view);
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
internal async Task OnGanttPrev()
|
|
{
|
|
if (scheduler?.SelectedView == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InvalidateTimelineCache();
|
|
scheduler.CurrentDate = scheduler.SelectedView.Prev();
|
|
await scheduler.Reload();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
internal async Task OnGanttNext()
|
|
{
|
|
if (scheduler?.SelectedView == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InvalidateTimelineCache();
|
|
scheduler.CurrentDate = scheduler.SelectedView.Next();
|
|
await scheduler.Reload();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
internal async Task OnGanttToday()
|
|
{
|
|
if (scheduler == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InvalidateTimelineCache();
|
|
scheduler.CurrentDate = DateTime.Today;
|
|
await scheduler.Reload();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Auto-calculates <see cref="ViewStart"/> and <see cref="ViewEnd"/> and selects the best zoom level
|
|
/// so that all tasks fit in the visible timeline.
|
|
/// </summary>
|
|
public async Task ZoomToFit()
|
|
{
|
|
if (scheduler == null) return;
|
|
|
|
var data = (IEnumerable<TItem>?)grid?.View ?? Data ?? Enumerable.Empty<TItem>();
|
|
DateTime? min = null, max = null;
|
|
|
|
var sg = GetStartGetter();
|
|
var eg = GetEndGetter();
|
|
if (sg == null || eg == null) return;
|
|
|
|
foreach (var item in data)
|
|
{
|
|
var s = ToDateTime(sg(item));
|
|
var e = ToDateTime(eg(item));
|
|
if (s.HasValue)
|
|
{
|
|
min = min.HasValue ? (s.Value < min.Value ? s.Value : min.Value) : s.Value;
|
|
}
|
|
if (e.HasValue)
|
|
{
|
|
max = max.HasValue ? (e.Value > max.Value ? e.Value : max.Value) : e.Value;
|
|
}
|
|
}
|
|
|
|
if (!min.HasValue || !max.HasValue) return;
|
|
|
|
var span = max.Value - min.Value;
|
|
GanttZoomLevel bestZoom;
|
|
if (span.TotalDays <= 2)
|
|
bestZoom = GanttZoomLevel.Day;
|
|
else if (span.TotalDays <= 60)
|
|
bestZoom = GanttZoomLevel.Week;
|
|
else if (span.TotalDays <= 180)
|
|
bestZoom = GanttZoomLevel.Month;
|
|
else
|
|
bestZoom = GanttZoomLevel.Year;
|
|
|
|
var pad = bestZoom switch
|
|
{
|
|
GanttZoomLevel.Day => 1,
|
|
GanttZoomLevel.Week => 3,
|
|
GanttZoomLevel.Month => 7,
|
|
GanttZoomLevel.Year => 14,
|
|
_ => 3
|
|
};
|
|
|
|
ViewStart = min.Value.Date.AddDays(-pad);
|
|
ViewEnd = max.Value.Date.AddDays(pad);
|
|
ZoomLevel = bestZoom;
|
|
InvalidateTimelineCache();
|
|
|
|
var view = GetViewForZoom(bestZoom);
|
|
if (view != null)
|
|
{
|
|
scheduler.CurrentDate = min.Value.Date;
|
|
await scheduler.SelectView(view);
|
|
}
|
|
await scheduler.Reload();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
internal async Task OnGanttZoomToFit()
|
|
{
|
|
await ZoomToFit();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Task Mouse Event Handlers
|
|
|
|
internal string? GanttElementId => GetId();
|
|
|
|
internal async Task HandleTaskBarInteraction(AppointmentData appointment, string mode, DateTime newStart, DateTime newEnd)
|
|
{
|
|
if (appointment.Data is TItem item)
|
|
{
|
|
var args = new GanttTaskMovedEventArgs<TItem> { Data = item, NewStart = newStart, NewEnd = newEnd };
|
|
if (mode == "move")
|
|
{
|
|
await TaskMove.InvokeAsync(args);
|
|
}
|
|
else
|
|
{
|
|
await TaskResize.InvokeAsync(args);
|
|
}
|
|
|
|
InvalidateTimelineCache();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
internal void OnTaskMouseEnter(SchedulerAppointmentMouseEventArgs<TItem> args)
|
|
{
|
|
TaskMouseEnter.InvokeAsync(new GanttTaskMouseEventArgs<TItem> { Element = args.Element, Data = args.Data, ClientX = args.ClientX, ClientY = args.ClientY });
|
|
}
|
|
|
|
internal void OnTaskMouseLeave(SchedulerAppointmentMouseEventArgs<TItem> args)
|
|
{
|
|
TaskMouseLeave.InvokeAsync(new GanttTaskMouseEventArgs<TItem> { Element = args.Element, Data = args.Data, ClientX = args.ClientX, ClientY = args.ClientY });
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region DataGrid Event Handlers
|
|
|
|
private void OnGridRowRender(RowRenderEventArgs<TItem> args)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(IdProperty) && !string.IsNullOrWhiteSpace(ParentIdProperty))
|
|
{
|
|
if (idGetter != null)
|
|
{
|
|
var query = (Data ?? Enumerable.Empty<TItem>()).AsQueryable()
|
|
.Where($"it => it.{ParentIdProperty} == @0", new object[] { idGetter(args.Data!) });
|
|
|
|
if (visibleIds != null)
|
|
{
|
|
query = query.Where(item => visibleIds.Contains(idGetter(item)));
|
|
}
|
|
|
|
args.Expandable = query.Any();
|
|
}
|
|
}
|
|
|
|
// Set row height
|
|
args.Attributes["style"] = $"height:{RowHeightPx}px;";
|
|
|
|
// Also invoke user's RowRender if provided
|
|
RowRender?.Invoke(args);
|
|
}
|
|
|
|
private async Task OnGridRowExpand(TItem item)
|
|
{
|
|
InvalidateTimelineCache();
|
|
await RowExpand.InvokeAsync(item);
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private async Task OnGridRowCollapse(TItem item)
|
|
{
|
|
InvalidateTimelineCache();
|
|
await RowCollapse.InvokeAsync(item);
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private async Task OnGridSort(DataGridColumnSortEventArgs<TItem> args)
|
|
{
|
|
await Sort.InvokeAsync(args);
|
|
InvalidateTimelineCache();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private async Task OnGridFilter(DataGridColumnFilterEventArgs<TItem> args)
|
|
{
|
|
await Filter.InvokeAsync(args);
|
|
InvalidateTimelineCache();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private async Task OnGridFilterCleared(DataGridColumnFilterEventArgs<TItem> args)
|
|
{
|
|
await FilterCleared.InvokeAsync(args);
|
|
InvalidateTimelineCache();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
IEnumerable<TItem>? pagedData;
|
|
int gridCount;
|
|
string? currentFilter;
|
|
HashSet<object>? visibleIds;
|
|
|
|
private void ComputeVisibleIds(string filter)
|
|
{
|
|
if (idGetter == null || parentIdGetter == null)
|
|
{
|
|
visibleIds = null;
|
|
return;
|
|
}
|
|
|
|
var allData = (Data ?? Enumerable.Empty<TItem>()).AsQueryable();
|
|
|
|
var parentLookup = new Dictionary<object, object?>();
|
|
foreach (var item in allData)
|
|
{
|
|
var id = idGetter(item);
|
|
if (id != null)
|
|
{
|
|
parentLookup[id] = parentIdGetter(item);
|
|
}
|
|
}
|
|
|
|
var matchingItems = allData.Where(filter);
|
|
|
|
visibleIds = new HashSet<object>();
|
|
foreach (var item in matchingItems)
|
|
{
|
|
var currentId = idGetter(item);
|
|
while (currentId != null && visibleIds.Add(currentId))
|
|
{
|
|
if (parentLookup.TryGetValue(currentId, out var parentId) && parentId != null)
|
|
{
|
|
currentId = parentId;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task OnGridLoadData(LoadDataArgs args)
|
|
{
|
|
currentFilter = args.Filter;
|
|
|
|
var query = (Data ?? Enumerable.Empty<TItem>()).AsQueryable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(ParentIdProperty))
|
|
{
|
|
query = query.Where($"it => it.{ParentIdProperty} == null");
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(args.Filter))
|
|
{
|
|
ComputeVisibleIds(args.Filter);
|
|
|
|
if (visibleIds != null)
|
|
{
|
|
query = query.Where(item => visibleIds.Contains(idGetter!(item)));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
visibleIds = null;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(args.OrderBy))
|
|
{
|
|
query = query.OrderBy(args.OrderBy);
|
|
}
|
|
|
|
gridCount = query.Count();
|
|
|
|
if (args.Skip.HasValue)
|
|
{
|
|
query = query.Skip(args.Skip.Value);
|
|
}
|
|
|
|
if (args.Top.HasValue)
|
|
{
|
|
query = query.Take(args.Top.Value);
|
|
}
|
|
|
|
pagedData = query.ToList();
|
|
|
|
InvalidateTimelineCache();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
async Task OnGridLoadChildData(DataGridLoadChildDataEventArgs<TItem> args)
|
|
{
|
|
var query = (Data ?? Enumerable.Empty<TItem>()).AsQueryable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(IdProperty) && !string.IsNullOrWhiteSpace(ParentIdProperty))
|
|
{
|
|
if (idGetter != null)
|
|
{
|
|
query = query.Where($"it => it.{ParentIdProperty} == @0", new object[] { idGetter(args.Item!)! });
|
|
}
|
|
}
|
|
|
|
if (visibleIds != null && idGetter != null)
|
|
{
|
|
query = query.Where(item => visibleIds.Contains(idGetter(item)));
|
|
}
|
|
|
|
args.Data = query;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expands a range of rows.
|
|
/// </summary>
|
|
/// <param name="items">The range of rows.</param>
|
|
public async Task ExpandRows(IEnumerable<TItem> items)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(items);
|
|
if (grid != null)
|
|
{
|
|
await grid.ExpandRows(items);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|