using System.Diagnostics.CodeAnalysis; using System.Reflection; using Connected.Annotations; using Connected.Extensions; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Web; namespace Connected.Components; // note: the MudTable code is split. Everything depending on the type parameter T of MudTable is here in MudTable public partial class Table<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T> : TableBase { /// /// Defines how a table row looks like. Use MudTd to define the table cells and their content. /// [Parameter] [Category(CategoryTypes.Table.Rows)] public RenderFragment RowTemplate { get; set; } /// /// Row Child content of the component. /// [Parameter] [Category(CategoryTypes.Table.Rows)] public RenderFragment ChildRowContent { get; set; } /// /// Defines how a table row looks like in edit mode (for selected row). Use MudTd to define the table cells and their content. /// [Parameter] [Category(CategoryTypes.Table.Editing)] public RenderFragment RowEditingTemplate { get; set; } #region Code for column based approach /// /// Defines how a table column looks like. Columns components should inherit from MudBaseColumn /// [Parameter] [Category(CategoryTypes.Table.Behavior)] public RenderFragment Columns { get; set; } /// /// Comma separated list of columns to show if there is no templates defined /// [Parameter] [Category(CategoryTypes.Table.Behavior)] public string QuickColumns { get; set; } // Workaround because "where T : new()" didn't work with Blazor components // T must have a default constructor, otherwise we cannot show headers when Items collection // is empty protected T Def { get { T t1 = default; if (t1 == null) { return Activator.CreateInstance(); } else { return default; } } } /// /// Creates a default Column renderfragment if there is no templates defined /// protected override void OnInitialized() { if (Columns == null && RowTemplate == null && RowEditingTemplate == null) { string[] quickcolumnslist = null; if (!QuickColumns.IsEmpty()) { quickcolumnslist = QuickColumns.Split(","); } // Create template from T Columns = context => builder => { var myType = context.GetType(); IList propertylist = new List(myType.GetProperties().Where(p => p.PropertyType.IsPublic)); if (quickcolumnslist == null) { foreach (var propinfo in propertylist) { BuildMudColumnTemplateItem(context, builder, propinfo); } } else { foreach (var colname in quickcolumnslist) { var propinfo = propertylist.SingleOrDefault(pl => pl.Name == colname); if (propinfo != null) { BuildMudColumnTemplateItem(context, builder, propinfo); } } } }; } } private static void BuildMudColumnTemplateItem(T context, RenderTreeBuilder builder, PropertyInfo propinfo) { if (propinfo.PropertyType.IsPrimitive || propinfo.PropertyType == typeof(string)) { builder.OpenComponent>(0); builder.AddAttribute(1, "Value", propinfo.GetValue(context)?.ToString()); builder.AddAttribute(2, "HeaderText", propinfo.Name); builder.CloseComponent(); } } #endregion /// /// Defines the table body content when there are no matching records found /// [Parameter] [Category(CategoryTypes.Table.Data)] public RenderFragment NoRecordsContent { get; set; } /// /// Defines the table body content the table has no rows and is loading /// [Parameter] [Category(CategoryTypes.Table.Data)] public RenderFragment LoadingContent { get; set; } /// /// Defines if the table has a horizontal scrollbar. /// [Parameter] [Category(CategoryTypes.Table.Behavior)] public bool HorizontalScrollbar { get; set; } internal string GetHorizontalScrollbarStyle() => HorizontalScrollbar ? ";display: block; overflow-x: auto;" : string.Empty; /// /// The data to display in the table. MudTable will render one row per item /// /// [Parameter] [Category(CategoryTypes.Table.Data)] public IEnumerable Items { get => _items; set { if (_items == value) return; _items = value; if (Context?.PagerStateHasChanged != null) InvokeAsync(Context.PagerStateHasChanged); } } /// /// A function that returns whether or not an item should be displayed in the table. You can use this to implement your own search function. /// [Parameter] [Category(CategoryTypes.Table.Filtering)] public Func Filter { get; set; } = null; /// /// Button click event. /// [Parameter] public EventCallback> OnRowClick { get; set; } internal override void FireRowClickEvent(MouseEventArgs args, Tr row, object o) { var item = default(T); try { item = (T)o; } catch (Exception) { /*ignore*/} OnRowClick.InvokeAsync(new TableRowClickEventArgs() { MouseEventArgs = args, Row = row, Item = item, }); } /// /// Returns the class that will get joined with RowClass. Takes the current item and row index. /// [Parameter] [Category(CategoryTypes.Table.Rows)] public Func RowClassFunc { get; set; } /// /// Returns the style that will get joined with RowStyle. Takes the current item and row index. /// [Parameter] [Category(CategoryTypes.Table.Rows)] public Func RowStyleFunc { get; set; } /// /// Returns the item which was last clicked on in single selection mode (that is, if MultiSelection is false) /// [Parameter] [Category(CategoryTypes.Table.Selecting)] public T SelectedItem { get => _selectedItem; set { if (_comparer != null && _comparer.Equals(SelectedItem, value)) return; else if (EqualityComparer.Default.Equals(SelectedItem, value)) return; _selectedItem = value; SelectedItemChanged.InvokeAsync(value); } } private T _selectedItem; /// /// Callback is called when a row has been clicked and returns the selected item. /// [Parameter] public EventCallback SelectedItemChanged { get; set; } /// /// If MultiSelection is true, this returns the currently selected items. You can bind this property and the initial content of the HashSet you bind it to will cause these rows to be selected initially. /// [Parameter] [Category(CategoryTypes.Table.Selecting)] public HashSet SelectedItems { get { if (!MultiSelection) if (_selectedItem is null) return new HashSet(Array.Empty(), _comparer); else return new HashSet(new T[] { _selectedItem }, _comparer); else return Context.Selection; } set { if (value == Context.Selection) return; if (value == null) { if (Context.Selection.Count == 0) return; Context.Selection = new HashSet(_comparer); } else Context.Selection = value; SelectedItemsChanged.InvokeAsync(Context.Selection); InvokeAsync(StateHasChanged); } } /// /// The Comparer to use for comparing selected items internally. /// [Parameter] [Category(CategoryTypes.FormComponent.Behavior)] public IEqualityComparer Comparer { get => _comparer; set { if (value == _comparer) return; _comparer = value; // Apply comparer and (selected values are refreshed in the Context.Comparer setter) Context.Comparer = _comparer; } } private IEqualityComparer _comparer; /// /// Callback is called whenever items are selected or deselected in multi selection mode. /// [Parameter] public EventCallback> SelectedItemsChanged { get; set; } private TableGroupDefinition _groupBy; /// /// Defines data grouping parameters. It can has N hierarchical levels /// [Parameter] [Category(CategoryTypes.Table.Grouping)] public TableGroupDefinition GroupBy { get => _groupBy; set { _groupBy = value; if (_groupBy != null) _groupBy.Context = Context; } } /// /// Defines how a table grouping row header looks like. It works only when GroupBy is not null. Use MudTd to define the table cells and their content. /// [Parameter] [Category(CategoryTypes.Table.Grouping)] public RenderFragment> GroupHeaderTemplate { get; set; } /// /// Defines custom CSS classes for using on Group Header's MudTr. /// [Parameter] [Category(CategoryTypes.Table.Grouping)] public string GroupHeaderClass { get; set; } /// /// Defines custom styles for using on Group Header's MudTr. /// [Parameter] [Category(CategoryTypes.Table.Grouping)] public string GroupHeaderStyle { get; set; } /// /// Defines custom CSS classes for using on Group Footer's MudTr. /// [Parameter] [Category(CategoryTypes.Table.Grouping)] public string GroupFooterClass { get; set; } /// /// Defines custom styles for using on Group Footer's MudTr. /// [Parameter] [Category(CategoryTypes.Table.Grouping)] public string GroupFooterStyle { get; set; } /// /// Defines how a table grouping row footer looks like. It works only when GroupBy is not null. Use MudTd to define the table cells and their content. /// [Parameter] [Category(CategoryTypes.Table.Grouping)] public RenderFragment> GroupFooterTemplate { get; set; } private IEnumerable _preEditSort { get; set; } = null; private bool _hasPreEditSort => _preEditSort != null; public IEnumerable FilteredItems { get { if (IsEditing && _hasPreEditSort) return _preEditSort; if (ServerData != null) { _preEditSort = _server_data.Items.ToList(); return _preEditSort; } if (Filter == null) { _preEditSort = Context.Sort(Items).ToList(); return _preEditSort; } _preEditSort = Context.Sort(Items.Where(Filter)).ToList(); return _preEditSort; } } protected IEnumerable CurrentPageItems { get { if (@PagerContent == null) return FilteredItems; // we have no pagination if (ServerData == null) { var filteredItemCount = GetFilteredItemsCount(); int lastPageNo; if (filteredItemCount == 0) lastPageNo = 0; else lastPageNo = (filteredItemCount / RowsPerPage) - (filteredItemCount % RowsPerPage == 0 ? 1 : 0); CurrentPage = lastPageNo < CurrentPage ? lastPageNo : CurrentPage; } return GetItemsOfPage(CurrentPage, RowsPerPage); } } protected IEnumerable GetItemsOfPage(int n, int pageSize) { if (n < 0 || pageSize <= 0) return Array.Empty(); if (ServerData != null) return _server_data.Items; return FilteredItems.Skip(n * pageSize).Take(pageSize); } protected override int NumPages { get { if (ServerData != null) return (int)Math.Ceiling(_server_data.TotalItems / (double)RowsPerPage); return (int)Math.Ceiling(FilteredItems.Count() / (double)RowsPerPage); } } public override int GetFilteredItemsCount() { if (ServerData != null) return _server_data.TotalItems; return FilteredItems.Count(); } public override void SetSelectedItem(object item) { SelectedItem = item.As(); } public override void SetEditingItem(object item) { if (!ReferenceEquals(_editingItem, item)) _editingItem = item; } public override bool ContainsItem(object item) { var t = item.As(); if (t is null) return false; return FilteredItems?.Contains(t) ?? false; } public override void UpdateSelection() => SelectedItemsChanged.InvokeAsync(SelectedItems); public override TableContext TableContext { get { Context.Table = this; Context.TableStateHasChanged = this.StateHasChanged; return Context; } } // TableContext provides shared functionality between all table sub-components public TableContext Context { get; } = new TableContext(); private void OnRowCheckboxChanged(bool value, T item) { if (value) Context.Selection.Add(item); else Context.Selection.Remove(item); SelectedItemsChanged.InvokeAsync(SelectedItems); } internal override void OnHeaderCheckboxClicked(bool value) { if (!value) Context.Selection.Clear(); else { foreach (var item in FilteredItems) Context.Selection.Add(item); } Context.UpdateRowCheckBoxes(false); SelectedItemsChanged.InvokeAsync(SelectedItems); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) await InvokeServerLoadFunc(); TableContext.UpdateRowCheckBoxes(); await base.OnAfterRenderAsync(firstRender); } /// /// Supply an async function which (re)loads filtered, paginated and sorted data from server. /// Table will await this func and update based on the returned TableData. /// Used only with ServerData /// [Parameter] [Category(CategoryTypes.Table.Data)] public Func>> ServerData { get; set; } internal override bool HasServerData => ServerData != null; TableData _server_data = new() { TotalItems = 0, Items = Array.Empty() }; private IEnumerable _items; internal override async Task InvokeServerLoadFunc() { if (ServerData == null) return; Loading = true; var label = Context.CurrentSortLabel; var state = new TableState { Page = CurrentPage, PageSize = RowsPerPage, SortDirection = Context.SortDirection, SortLabel = label?.SortLabel }; _server_data = await ServerData(state); if (CurrentPage * RowsPerPage > _server_data.TotalItems) CurrentPage = 0; Loading = false; StateHasChanged(); Context?.PagerStateHasChanged?.Invoke(); } protected override void OnAfterRender(bool firstRender) { base.OnAfterRender(firstRender); if (!firstRender) Context?.PagerStateHasChanged?.Invoke(); } /// /// Call this to reload the server-filtered, -sorted and -paginated items /// public Task ReloadServerData() { return InvokeServerLoadFunc(); } internal override bool IsEditable { get => (RowEditingTemplate != null) || (Columns != null); } //GROUPING: private IEnumerable> GroupItemsPage { get { return GetItemsOfGroup(GroupBy, CurrentPageItems); } } internal IEnumerable> GetItemsOfGroup(TableGroupDefinition parent, IEnumerable sourceList) { if (parent == null || sourceList == null) return new List>(); return sourceList.GroupBy(parent.Selector).ToList(); } internal void OnGroupHeaderCheckboxClicked(bool value, IEnumerable items) { if (value) { foreach (var item in items) Context.Selection.Add(item); } else { foreach (var item in items) Context.Selection.Remove(item); } Context.UpdateRowCheckBoxes(false); SelectedItemsChanged.InvokeAsync(SelectedItems); } }