Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,12 @@
Gets or sets the content to be rendered for each row in the table.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.SelectColumn`1.RestrictToCheckbox">
<summary>
Gets or sets whether the selection of rows is restricted to the checkbox cells (true) or if the whole row can be clicked to toggled the selection of rows (false).
⚠ Setting this to true will stop the <see cref="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.OnRowClick"/> from being invoked, when a cell in this column is clicked.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.SelectColumn`1.SelectAllDisabled">
<summary>
Gets or sets whether the [All] checkbox is disabled (not clickable).
Expand Down Expand Up @@ -1725,6 +1731,11 @@
Gets or sets a callback when a row is clicked.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.OnCellClick">
<summary>
Gets or sets a callback when a cell is clicked.
</summary>
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1.OnRowDoubleClick">
<summary>
Gets or sets a callback when a row is double-clicked.
Expand Down Expand Up @@ -1855,6 +1866,9 @@
Gets or sets the owning <see cref="T:Microsoft.FluentUI.AspNetCore.Components.FluentDataGrid`1"/> component
</summary>
</member>
<member name="M:Microsoft.FluentUI.AspNetCore.Components.FluentDataGridCell`1.HandleOnCellClickAsync">
<summary />
</member>
<member name="P:Microsoft.FluentUI.AspNetCore.Components.FluentDataGridRow`1.Item">
<summary>
Gets or sets the reference to the item that holds this row's values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@
<FluentCheckbox @bind-Value="@UseSelectedItems"
@bind-Value:after="@(() => ResetSelectItems())"
Label="Use `SelectedItems` property" />
<FluentCheckbox @bind-Value="@RestrictToCheckbox"
@bind-Value:after="@(() => ResetSelectItems())"
Label="Use `RestrictToCheckbox` property" />
</FluentStack>

@if (UseSelectedItems)
{
@* Sample using SelectedItems *@
<div>Using SelectedItems</div>

<FluentDataGrid Items="@People" ShowHover="true" TGridItem="Person">
<SelectColumn TGridItem="Person"
<FluentDataGrid Items="@People" ShowHover="true" TGridItem="Person"
OnRowClick="@(row => Console.WriteLine($"Row {row.RowIndex} clicked"))"
OnCellClick="@(cell => Console.WriteLine($"Cell {cell.GridColumn} clicked"))">
<SelectColumn @ref="SelectColumn"
TGridItem="Person"
SelectMode="@Mode"
RestrictToCheckbox="@RestrictToCheckbox"
@bind-SelectedItems="@SelectedItems" />
<PropertyColumn Width="100px" Property="@(p => p.PersonId)" Title="ID" />
<PropertyColumn Width="300px" Property="@(p => p.Name)" />
Expand All @@ -30,9 +37,13 @@ else
@* Sample using Property and OnSelect *@
<div>Using Property and OnSelect</div>

<FluentDataGrid Items="@People" ShowHover="true" TGridItem="Person">
<SelectColumn TGridItem="Person"
<FluentDataGrid Items="@People" ShowHover="true" TGridItem="Person"
OnRowClick="@(row => Console.WriteLine($"Row {row.RowIndex} clicked"))"
OnCellClick="@(cell => Console.WriteLine($"Cell {cell.GridColumn} clicked"))">
<SelectColumn @ref="SelectColumn"
TGridItem="Person"
SelectMode="@Mode"
RestrictToCheckbox="@RestrictToCheckbox"
Property="@(e => e.Selected)"
OnSelect="@(e => e.Item.Selected = e.Selected)"
SelectAll="@(People.All(p => p.Selected))"
Expand All @@ -49,10 +60,13 @@ else
}

@code {
SelectColumn<Person>? SelectColumn;

bool UseSelectedItems = true;
DataGridSelectMode Mode = DataGridSelectMode.Single;

IEnumerable<Person> SelectedItems = People.Where(p => p.Selected);
bool RestrictToCheckbox = false;

record Person(int PersonId, string Name, DateOnly BirthDate)
{
Expand All @@ -72,7 +86,6 @@ else
private void ResetSelectItems()
{
People.ToList().ForEach(i => i.Selected = false);
People.First().Selected = true;
SelectedItems = People.Where(p => p.Selected);
SelectColumn?.ClearSelection();
}
}
7 changes: 7 additions & 0 deletions src/Core/Components/DataGrid/Columns/SelectColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public SelectColumn()
[Parameter]
public RenderFragment<TGridItem> ChildContent { get; set; }

/// <summary>
/// Gets or sets whether the selection of rows is restricted to the checkbox cells (true) or if the whole row can be clicked to toggled the selection of rows (false).
/// ⚠ Setting this to true will stop the <see cref="FluentDataGrid{TGridItem}.OnRowClick"/> from being invoked, when a cell in this column is clicked.
/// </summary>
[Parameter]
public bool RestrictToCheckbox { get; set; } = false;

/// <summary>
/// Gets or sets whether the [All] checkbox is disabled (not clickable).
/// </summary>
Expand Down
12 changes: 6 additions & 6 deletions src/Core/Components/DataGrid/FluentDataGrid.razor
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@
var rowStyle = RowStyle?.Invoke(item) ?? null;
Loading = false;
<FluentDataGridRow @key="@(ItemKey(item))" GridTemplateColumns=@_internalGridTemplateColumns aria-rowindex="@rowIndex" Class="@rowClass" Style="@rowStyle" TGridItem="TGridItem" Item="@item">
@for (var colIndex = 0; colIndex < _columns.Count; colIndex++)
@for (var colIndex = 0; colIndex < Columns.Count; colIndex++)
{
var col = _columns[colIndex];
var col = Columns[colIndex];

string? tooltip = col.Tooltip ? @col.RawCellContent(item) : null;

Expand All @@ -110,9 +110,9 @@
string? _rowsDataSize = $"height: {ItemSize}px";

<FluentDataGridRow GridTemplateColumns=@GridTemplateColumns aria-rowindex="@(placeholderContext.Index + 1)" Style="@_rowsDataSize" TGridItem="TGridItem">
@for (var colIndex = 0; colIndex < _columns.Count; colIndex++)
@for (var colIndex = 0; colIndex < Columns.Count; colIndex++)
{
var col = _columns[colIndex];
var col = Columns[colIndex];

<FluentDataGridCell Class="@("grid-cell-placeholder " + @ColumnClass(col))" Style="@col.Style" @key="@col" GridColumn=@(colIndex+1) TGridItem="TGridItem">
@((RenderFragment)(__builder => col.RenderPlaceholderContent(__builder, placeholderContext)))
Expand All @@ -124,9 +124,9 @@

private void RenderColumnHeaders(RenderTreeBuilder __builder)
{
@for (var colIndex = 0; colIndex < _columns.Count; colIndex++)
@for (var colIndex = 0; colIndex < Columns.Count; colIndex++)
{
var col = _columns[colIndex];
var col = Columns[colIndex];
string CellId = Identifier.NewId();
if (_sortByColumn == col)
col.ShowSortIcon = true;
Expand Down
32 changes: 19 additions & 13 deletions src/Core/Components/DataGrid/FluentDataGrid.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
[Parameter]
public EventCallback<FluentDataGridRow<TGridItem>> OnRowClick { get; set; }

/// <summary>
/// Gets or sets a callback when a cell is clicked.
/// </summary>
[Parameter]
public EventCallback<FluentDataGridCell<TGridItem>> OnCellClick { get; set; }

/// <summary>
/// Gets or sets a callback when a row is double-clicked.
/// </summary>
Expand Down Expand Up @@ -175,7 +181,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
/// <summary>
/// Gets the first (optional) SelectColumn
/// </summary>
internal IEnumerable<SelectColumn<TGridItem>> SelectColumns => _columns.Where(col => col is SelectColumn<TGridItem>).Cast< SelectColumn<TGridItem>>();
internal IEnumerable<SelectColumn<TGridItem>> SelectColumns => Columns.Where(col => col is SelectColumn<TGridItem>).Cast< SelectColumn<TGridItem>>();

private ElementReference? _gridReference;
private Virtualize<(int, TGridItem)>? _virtualizeComponent;
Expand All @@ -187,7 +193,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
// We cascade the InternalGridContext to descendants, which in turn call it to add themselves to _columns
// This happens on every render so that the column list can be updated dynamically
private readonly InternalGridContext<TGridItem> _internalGridContext;
private readonly List<ColumnBase<TGridItem>> _columns;
internal readonly List<ColumnBase<TGridItem>> Columns;
private bool _collectingColumns; // Columns might re-render themselves arbitrarily. We only want to capture them at a defined time.

// Tracking state for options and sorting
Expand Down Expand Up @@ -229,7 +235,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(DataGridRowFocusEventArgs))]
public FluentDataGrid()
{
_columns = [];
Columns = [];
_internalGridContext = new(this);
_currentPageItemsChanged = new(EventCallback.Factory.Create<PaginationState>(this, RefreshDataCoreAsync));
_renderColumnHeaders = RenderColumnHeaders;
Expand Down Expand Up @@ -279,7 +285,7 @@ protected override Task OnParametersSetAsync()
// We don't want to trigger the first data load until we've collected the initial set of columns,
// because they might perform some action like setting the default sort order, so it would be wasteful
// to have to re-query immediately
return (_columns.Count > 0 && mustRefreshData) ? RefreshDataCoreAsync() : Task.CompletedTask;
return (Columns.Count > 0 && mustRefreshData) ? RefreshDataCoreAsync() : Task.CompletedTask;
}

protected override async Task OnAfterRenderAsync(bool firstRender)
Expand Down Expand Up @@ -309,7 +315,7 @@ internal void AddColumn(ColumnBase<TGridItem> column, SortDirection? initialSort
{
if (_collectingColumns)
{
_columns.Add(column);
Columns.Add(column);

if (isDefaultSortColumn && _sortByColumn is null && initialSortDirection.HasValue)
{
Expand All @@ -322,23 +328,23 @@ internal void AddColumn(ColumnBase<TGridItem> column, SortDirection? initialSort

private void StartCollectingColumns()
{
_columns.Clear();
Columns.Clear();
_collectingColumns = true;
}

private void FinishCollectingColumns()
{
_collectingColumns = false;
_manualGrid = _columns.Count == 0;
_manualGrid = Columns.Count == 0;

if (!string.IsNullOrWhiteSpace(GridTemplateColumns) && _columns.Where(x => x is not SelectColumn<TGridItem>).Any(x => !string.IsNullOrWhiteSpace(x.Width)))
if (!string.IsNullOrWhiteSpace(GridTemplateColumns) && Columns.Where(x => x is not SelectColumn<TGridItem>).Any(x => !string.IsNullOrWhiteSpace(x.Width)))
{
throw new Exception("You can use either the 'GridTemplateColumns' parameter on the grid or the 'Width' property at the column level, not both.");
}

if (string.IsNullOrWhiteSpace(_internalGridTemplateColumns) && _columns.Any(x => !string.IsNullOrWhiteSpace(x.Width)))
if (string.IsNullOrWhiteSpace(_internalGridTemplateColumns) && Columns.Any(x => !string.IsNullOrWhiteSpace(x.Width)))
{
_internalGridTemplateColumns = string.Join(" ", _columns.Select(x => x.Width ?? "1fr"));
_internalGridTemplateColumns = string.Join(" ", Columns.Select(x => x.Width ?? "1fr"));
}

if (ResizableColumns)
Expand Down Expand Up @@ -376,7 +382,7 @@ public Task SortByColumnAsync(ColumnBase<TGridItem> column, SortDirection direct
/// <param name="direction">The direction of sorting. The default is <see cref="SortDirection.Auto"/>. If the value is <see cref="SortDirection.Auto"/>, then it will toggle the direction on each call.</param>
public Task SortByColumnAsync(string title, SortDirection direction = SortDirection.Auto)
{
var column = _columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false);
var column = Columns.FirstOrDefault(c => c.Title?.Equals(title, StringComparison.InvariantCultureIgnoreCase) ?? false);

if (column is not null)
{
Expand All @@ -393,9 +399,9 @@ public Task SortByColumnAsync(string title, SortDirection direction = SortDirect
/// <param name="direction">The direction of sorting. The default is <see cref="SortDirection.Auto"/>. If the value is <see cref="SortDirection.Auto"/>, then it will toggle the direction on each call.</param>
public Task SortByColumnAsync(int index, SortDirection direction = SortDirection.Auto)
{
if (index >= 0 && index < _columns.Count)
if (index >= 0 && index < Columns.Count)
{
return SortByColumnAsync(_columns[index], direction);
return SortByColumnAsync(Columns[index], direction);
}

return Task.CompletedTask;
Expand Down
1 change: 1 addition & 0 deletions src/Core/Components/DataGrid/FluentDataGridCell.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
grid-column=@GridColumn
class="@Class"
style="@StyleValue"
@onclick="@HandleOnCellClickAsync"
@attributes="AdditionalAttributes">
@ChildContent
</fluent-data-grid-cell>
19 changes: 19 additions & 0 deletions src/Core/Components/DataGrid/FluentDataGridCell.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,23 @@ protected override void OnInitialized()

public void Dispose() => Owner.Unregister(this);

/// <summary />
internal async Task HandleOnCellClickAsync()
{
if (GridContext.Grid.OnCellClick.HasDelegate)
{
await GridContext.Grid.OnCellClick.InvokeAsync(this);
}

if (CellType == DataGridCellType.Default && Owner.Owner.Grid.SelectColumns.Any(selColumn => selColumn.RestrictToCheckbox))
{
foreach (var selColumn in Owner.Owner.Grid.SelectColumns)
{
if (selColumn != null && selColumn.RestrictToCheckbox is true && Owner.Owner.Grid.Columns.IndexOf(selColumn) == GridColumn - 1)
{
await selColumn.AddOrRemoveSelectedItemAsync(Item);
}
}
}
}
}
20 changes: 13 additions & 7 deletions src/Core/Components/DataGrid/FluentDataGridRow.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
public partial class FluentDataGridRow<TGridItem> : FluentComponentBase, IHandleEvent, IDisposable
{
internal string RowId { get; set; } = string.Empty;
private readonly Dictionary<string, FluentDataGridCell<TGridItem>> cells = [];
internal Dictionary<string, FluentDataGridCell<TGridItem>> Cells { get; } = [];

/// <summary>
/// Gets or sets the reference to the item that holds this row's values.
Expand Down Expand Up @@ -56,7 +56,7 @@ public partial class FluentDataGridRow<TGridItem> : FluentComponentBase, IHandle
/// Gets or sets the owning <see cref="FluentDataGrid{TItem}"/> component
/// </summary>
[CascadingParameter]
private InternalGridContext<TGridItem> Owner { get; set; } = default!;
internal InternalGridContext<TGridItem> Owner { get; set; } = default!;

protected string? ClassValue => new CssBuilder(Class)
.AddClass("hover", when: Owner.Grid.ShowHover)
Expand All @@ -80,18 +80,18 @@ internal void Register(FluentDataGridCell<TGridItem> cell)
{

cell.CellId = $"c{Owner.GetNextCellId()}";
cells.Add(cell.CellId, cell);
Cells.Add(cell.CellId, cell);
}

internal void Unregister(FluentDataGridCell<TGridItem> cell)
{
cells.Remove(cell.CellId!);
Cells.Remove(cell.CellId!);
}

private async Task HandleOnCellFocusAsync(DataGridCellFocusEventArgs args)
{
var cellId = args.CellId;
if (cells.TryGetValue(cellId!, out var cell))
if (Cells.TryGetValue(cellId!, out var cell))
{
if (cell != null && cell.CellType == DataGridCellType.Default)
{
Expand All @@ -114,7 +114,10 @@ internal async Task HandleOnRowClickAsync(string rowId)
{
foreach (var selColumn in Owner.Grid.SelectColumns)
{
await selColumn.AddOrRemoveSelectedItemAsync(Item);
if (!selColumn.RestrictToCheckbox)
{
await selColumn.AddOrRemoveSelectedItemAsync(Item);
}
}
}
}
Expand Down Expand Up @@ -144,7 +147,10 @@ internal async Task HandleOnRowKeyDownAsync(string rowId, KeyboardEventArgs e)
{
foreach (var selColumn in Owner.Grid.SelectColumns)
{
await selColumn.AddOrRemoveSelectedItemAsync(Item);
if (!selColumn.RestrictToCheckbox)
{
await selColumn.AddOrRemoveSelectedItemAsync(Item);
}
}
}
}
Expand Down
Loading