764 lines
25 KiB
Plaintext
764 lines
25 KiB
Plaintext
@using System.Dynamic
|
|
@using System.Runtime.CompilerServices
|
|
@using System.Text
|
|
@using Microsoft.CSharp.RuntimeBinder
|
|
|
|
@inject IJSRuntime JsRuntime
|
|
|
|
@*
|
|
* dbMango
|
|
*
|
|
* Copyright 2025 Deutsche Bank AG
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*@
|
|
|
|
<style>
|
|
.tc-row-number {
|
|
color: @Night.secondary;
|
|
}
|
|
|
|
.align-left {
|
|
text-align: left;
|
|
}
|
|
|
|
.align-right {
|
|
text-align: right;
|
|
}
|
|
|
|
.totals-row {
|
|
font-weight: bold;
|
|
background-color: #305060;
|
|
}
|
|
</style>
|
|
|
|
<div class="mb-0 @Class">
|
|
<table class="@TableClass">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 25px !important;" class="p-0">
|
|
<div class="w-100 h-100 d-flex flex-column">
|
|
<button class="btn px-0 w-100 text-left" @onclick="TriggerFilterRow">
|
|
<span class="m-auto ui-icon-font ui-icon-sm icon-search-sm"></span>
|
|
</button>
|
|
@if (Filterable)
|
|
{
|
|
<span class="m-auto">#</span>
|
|
}
|
|
</div>
|
|
</th>
|
|
<CascadingValue Value="this">
|
|
@ChildContent
|
|
</CascadingValue>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
@if (DisplayedItems.Any())
|
|
{
|
|
var ix = 1 + (_currentPage - 1) * PageSize;
|
|
@foreach (var item in DisplayedItems)
|
|
{
|
|
<tr class="@(RowClass(item))">
|
|
<td class="tc-row-number">
|
|
@ix
|
|
</td>
|
|
@foreach (var column in Columns)
|
|
{
|
|
@if (column.Template == null && !string.IsNullOrEmpty(column.Field))
|
|
{
|
|
var val = GetLambdaForValue(column.Field)(item);
|
|
|
|
<td class="@column.Class @(CellClass(item, column))"
|
|
style="@(CellStyle(item, column))"
|
|
@onclick="@(() => OnSelectionChanged(item, column.Field))"
|
|
>
|
|
@ConvertToString(val, column.Format)
|
|
</td>
|
|
}
|
|
else if (column.Template != null)
|
|
{
|
|
<td class="@column.Class @(CellClass(item, column))"
|
|
style="@(CellStyle(item, column))"
|
|
@onclick="@(() => OnSelectionChanged(item, column.Field))">
|
|
@column.Template((item, column))
|
|
</td>
|
|
}
|
|
else
|
|
{
|
|
<td class="@column.Class @(CellClass(item, column))">
|
|
???
|
|
</td>
|
|
}
|
|
}
|
|
</tr>
|
|
ix++;
|
|
}
|
|
@if (Totals != null)
|
|
{
|
|
<tr>
|
|
<td class="totals-row">
|
|
Total
|
|
</td>
|
|
@foreach (var column in Columns)
|
|
{
|
|
if (column.ShowTotals)
|
|
{
|
|
var val = Totals.TryGet(column?.Name ?? "")?.Total;
|
|
|
|
<td class="@(GetCellClass("", val, column!)) totals-row">
|
|
@ConvertToString(val, column!.Format)
|
|
</td>
|
|
}
|
|
else
|
|
{
|
|
<td class="totals-row">
|
|
|
|
</td>
|
|
}
|
|
}
|
|
</tr>
|
|
}
|
|
}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>@TableFooter</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<nav aria-label="Page navigation">
|
|
<ul class="pagination ag-wadk ag-paging-panel mt-1" style="align-items: center; justify-content: flex-start;">
|
|
@if (PageSize <= Items?.Count)
|
|
{
|
|
<li class="page-item">
|
|
<a class="page-link cursor-pointer" @onclick=@(() => NavigateToPage(Direction.Back))>«</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link cursor-pointer" @onclick=@(() => NavigateToPage(Direction.Previous))>Previous</a>
|
|
</li>
|
|
@for (var i = _startPage; i <= _endPage; i++)
|
|
{
|
|
var currentPage = i;
|
|
<li class="page-item @(currentPage==_currentPage?"active":"")">
|
|
<a class="page-link cursor-pointer" @onclick=@(() => NavigateToPage(currentPage))>
|
|
@currentPage
|
|
</a>
|
|
</li>
|
|
}
|
|
<li class="page-item">
|
|
<a class="page-link cursor-pointer" @onclick=@(() => NavigateToPage(Direction.Next))>Next</a>
|
|
</li>
|
|
<li class="page-item">
|
|
<a class="page-link cursor-pointer" @onclick=@(() => NavigateToPage(Direction.Forward))>»</a>
|
|
</li>
|
|
}
|
|
<li class="page-item ml-2" style="color: @Night.secondary">
|
|
@(DisplayedItems?.Count() ?? 0) of total @Items?.Count
|
|
</li>
|
|
<li class="page-item ml-2 flex-pull-right">
|
|
<a class="page-link cursor-pointer" @onclick="@OnCopyAllToClipboard">
|
|
<span class="ui-icon-font icon-duplicate-document-sm"></span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@code {
|
|
public enum Direction
|
|
{
|
|
Back,
|
|
Previous,
|
|
Next,
|
|
Forward
|
|
}
|
|
|
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
|
[Parameter] public RenderFragment? TableFooter { get; set; }
|
|
|
|
[Parameter]
|
|
public IReadOnlyCollection<dynamic> Items
|
|
{
|
|
get;
|
|
set
|
|
{
|
|
//detect if the new list is actually different, this is a little basic, but handles deletes/adds
|
|
//to the list. Ideally a hash of the list is best, but because 'StateHasChanged' is being called so many times
|
|
//by some controls (ContextControl!) this logic
|
|
if (value == null || (value.Count == _itemCount && Equals(field, value)))
|
|
return;
|
|
|
|
field = value;
|
|
_itemCount = field.Count;
|
|
|
|
ActiveFilters.Clear();
|
|
ActiveFiltersExactMatch.Clear();
|
|
//reapply the filtering to the new items
|
|
Task.Run(ApplyColumnFilterAndSort);
|
|
}
|
|
} = [];
|
|
|
|
[Parameter] public int PageSize { get; set; } = 10;
|
|
[Parameter] public int PagerSize { get; set; } = 5;
|
|
[Parameter] public string? Class { get; set; }
|
|
[Parameter] public string? TableClass { get; set; }
|
|
[Parameter] public bool Filterable { get; set; }
|
|
[Parameter] public bool Selectable { get; set; }
|
|
[Parameter] public dynamic? SelectedItem { get; set; }
|
|
[Parameter] public string? SelectedColumn { get; set; }
|
|
[Parameter] public EventCallback<(dynamic row, string col)> SelectionChanged { get; set; }
|
|
[Parameter] public Func<dynamic, string> RowClass { get; set; } = _ => "";
|
|
[Parameter] public bool AbsoluteSort { get; set; } = true;
|
|
[Parameter] public IMinMaxCache? Totals { get; set; }
|
|
|
|
private string UniqueID { get;set;} = Guid.NewGuid().ToString();
|
|
|
|
private int _itemCount;
|
|
|
|
/// <summary>
|
|
/// Allow the user to specify a custom class per cell, this will completely override the default styling
|
|
/// </summary>
|
|
[Parameter] public Func<dynamic, TableColumnControl, string> CellClass { get; set; } = DefaultGetCellClass;
|
|
/// <summary>
|
|
/// Allow the user to specify a custom style per cell, this will be applied on top of normal styling
|
|
/// </summary>
|
|
[Parameter] public Func<dynamic, TableColumnControl, string> CellStyle { get; set; } = (_,_) => "";
|
|
|
|
/// <summary>
|
|
/// Allows the user to apply _Extra_ styling on top of the existing default styling
|
|
/// </summary>
|
|
[Parameter] public Func<dynamic, TableColumnControl, string>? GetCellClassCallback { get; set; }
|
|
|
|
[Parameter]
|
|
public List<dynamic> FilteredItems { get; set; } = [];
|
|
|
|
[Parameter]
|
|
public EventCallback<List<dynamic>> FilteredItemsChanged { get; set; }
|
|
|
|
[Parameter]
|
|
public string? CurrentSortColumn
|
|
{
|
|
get;
|
|
set
|
|
{
|
|
if (field == value)
|
|
return;
|
|
|
|
field = value;
|
|
CurrentSortColumnChanged.InvokeAsync(field);
|
|
}
|
|
} = null;
|
|
|
|
[Parameter]
|
|
public EventCallback<string> CurrentSortColumnChanged { get; set; }
|
|
|
|
public enum SortModeType
|
|
{
|
|
NoSort,
|
|
Ascending,
|
|
Descending,
|
|
AscendingAbsolute,
|
|
DescendingAbsolute
|
|
}
|
|
|
|
[Parameter]
|
|
public SortModeType SortMode
|
|
{
|
|
get;
|
|
set
|
|
{
|
|
if (field == value)
|
|
return;
|
|
|
|
field = value;
|
|
SortModeChanged.InvokeAsync(field);
|
|
}
|
|
} = SortModeType.NoSort;
|
|
|
|
[Parameter]
|
|
public EventCallback<SortModeType> SortModeChanged { get; set; }
|
|
|
|
//Column the table is currently sorted by.
|
|
|
|
protected IEnumerable<dynamic> DisplayedItems => FilteredItems?.Skip((_currentPage - 1) * PageSize).Take(PageSize) ?? [];
|
|
|
|
private int _totalPages;
|
|
private int _currentPage = 1;
|
|
private int _startPage;
|
|
private int _endPage;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
if (Items == null)
|
|
return;
|
|
|
|
await ApplyColumnFilterAndSort();
|
|
}
|
|
}
|
|
|
|
private async Task UpdatePage(int page)
|
|
{
|
|
_currentPage = page;
|
|
_totalPages = (int)Math.Ceiling((FilteredItems?.Count ?? 0) / (decimal)PageSize);
|
|
|
|
var sideWidth = (int)Math.Floor((PagerSize - 1) / 2d);
|
|
_startPage = _currentPage - sideWidth >= 1 ? _currentPage - sideWidth : 1;
|
|
_endPage = _currentPage + sideWidth <= _totalPages ? _currentPage + sideWidth : _totalPages;
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
#region sorting
|
|
|
|
//Direction the table is currently sorted by
|
|
|
|
/// <summary>
|
|
/// Returns a function that given a row item, will return the value for a specified field
|
|
/// </summary>
|
|
/// <param name="field"></param>
|
|
/// <returns></returns>
|
|
public static Func<dynamic?, dynamic?> GetLambdaForValue(string? field)
|
|
{
|
|
return i =>
|
|
{
|
|
if (string.IsNullOrEmpty(field))
|
|
{
|
|
return "";
|
|
}
|
|
switch (i)
|
|
{
|
|
case IDictionary<string, object> dict:
|
|
{
|
|
dict.TryGetValue(field, out var value);
|
|
return value;
|
|
}
|
|
case IDictionary<string, string> dict1:
|
|
{
|
|
dict1.TryGetValue(field, out var value);
|
|
return value;
|
|
}
|
|
case DynamicObject dyn:
|
|
{
|
|
return GetDynamicMember(dyn, field);
|
|
}
|
|
default:
|
|
return i?.GetType().GetProperty(field)?.GetValue(i, null);
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a function that given a row item, will return the absolute value for a specified field
|
|
/// </summary>
|
|
/// <param name="field"></param>
|
|
/// <returns></returns>
|
|
public static Func<dynamic?, dynamic?> GetLambdaForAbsValue(string? field)
|
|
{
|
|
var getValue = GetLambdaForValue(field);
|
|
return x =>
|
|
{
|
|
var val = getValue(x);
|
|
return val switch
|
|
{
|
|
double d => Math.Abs(d),
|
|
int i => Math.Abs(i),
|
|
long l => Math.Abs(l),
|
|
float f => Math.Abs(f),
|
|
_ => val
|
|
};
|
|
};
|
|
}
|
|
|
|
public static bool IsNumeric(object value) => value is double ||
|
|
value is int ||
|
|
value is long ||
|
|
value is uint ||
|
|
value is ulong;
|
|
|
|
/// <summary>
|
|
/// This is a rather weird way around the ability to create an Icomparer of dynamic
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
public abstract class GridComparerDynamic<T> : IComparer<T>
|
|
{
|
|
public abstract int Compare(T? row1, T? row2);
|
|
}
|
|
|
|
|
|
public class GridComparer(Func<dynamic?, dynamic?> methodLambda) : GridComparerDynamic<dynamic>
|
|
{
|
|
public override int Compare(object? row1, object? row2)
|
|
{
|
|
var row1Value = methodLambda(row1);
|
|
var row2Value = methodLambda(row2);
|
|
|
|
if (row1Value == null && row2Value == null) return 0;
|
|
//put any nulls at the end of the list
|
|
if (row1Value == null) return -1;
|
|
if (row2Value == null) return 1;
|
|
|
|
//its possible for the type to be different here
|
|
if (row1Value.GetType() != row2Value.GetType())
|
|
{
|
|
//if both types are at least numberic, then compare them as doubles
|
|
if (IsNumeric(row1Value) && IsNumeric(row2Value))
|
|
return ((double)row1Value).CompareTo((double)row2Value);
|
|
//fall back to comparing as strings
|
|
return row1Value.ToString().CompareTo(row2Value.ToString());
|
|
}
|
|
if (row1Value is IComparable)
|
|
return row1Value.CompareTo(row2Value);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
#region Filtering
|
|
|
|
protected readonly Dictionary<string, List<string>> ActiveFilters = new();
|
|
protected readonly Dictionary<string, bool> ActiveFiltersExactMatch = new();
|
|
|
|
|
|
public async Task SetColumnFilter(string columnName, List<string> filterValue, bool exactMatch = false)
|
|
{
|
|
if (ActiveFilters.ContainsKey(columnName) &&
|
|
ActiveFilters[columnName] == filterValue &&
|
|
ActiveFiltersExactMatch.ContainsKey(columnName) &&
|
|
ActiveFiltersExactMatch[columnName] == exactMatch)
|
|
return;
|
|
|
|
ActiveFilters[columnName] = filterValue;
|
|
ActiveFiltersExactMatch[columnName] = exactMatch;
|
|
if (filterValue.Count == 0)
|
|
{
|
|
ActiveFilters.Remove(columnName);
|
|
ActiveFiltersExactMatch.Remove(columnName);
|
|
}
|
|
|
|
_currentPage = 1;
|
|
|
|
await ApplyColumnFilterAndSort();
|
|
}
|
|
|
|
public async Task SetColumnSort(string columnName)
|
|
{
|
|
//Sorting against a column that is not currently sorted against.
|
|
if (columnName != CurrentSortColumn)
|
|
{
|
|
//Force order on the new column
|
|
CurrentSortColumn = columnName;
|
|
SortMode = AbsoluteSort
|
|
? SortModeType.DescendingAbsolute
|
|
: SortModeType.Descending;
|
|
}
|
|
else //Sorting against same column but in different direction
|
|
{
|
|
if (AbsoluteSort)
|
|
SortMode = SortMode switch
|
|
{
|
|
SortModeType.AscendingAbsolute => SortModeType.DescendingAbsolute,
|
|
SortModeType.DescendingAbsolute => SortModeType.AscendingAbsolute,
|
|
_ => SortModeType.DescendingAbsolute
|
|
};
|
|
else
|
|
SortMode = SortMode switch
|
|
{
|
|
SortModeType.Ascending => SortModeType.Descending,
|
|
SortModeType.Descending => SortModeType.Ascending,
|
|
_ => SortModeType.Descending
|
|
};
|
|
}
|
|
|
|
await ApplyColumnFilterAndSort();
|
|
}
|
|
|
|
private async Task ApplyColumnFilterAndSort()
|
|
{
|
|
IEnumerable<dynamic> fa = Items; // no copying needed
|
|
|
|
//create data filtering linq
|
|
foreach (var (column, value) in ActiveFilters)
|
|
{
|
|
var exactMatch = ActiveFiltersExactMatch[column];
|
|
fa = fa.Where(x =>
|
|
{
|
|
var tableColumnDef = Columns.FirstOrDefault(c => c.Field == column);
|
|
|
|
var colOriginalValue = GetLambdaForValue(column)(x);
|
|
|
|
var colValue = ConvertToString(colOriginalValue, tableColumnDef?.Format);
|
|
|
|
if (colValue != null)
|
|
{
|
|
var res = exactMatch
|
|
? value.Any(v => colValue.Equals(v))
|
|
: value.Any(v => colValue.Contains(v, StringComparison.OrdinalIgnoreCase));
|
|
return res;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
//add data sorting on top
|
|
fa = SortMode switch
|
|
{
|
|
SortModeType.NoSort => fa,
|
|
SortModeType.Ascending => fa.OrderBy (x => x, new GridComparer(GetLambdaForValue(CurrentSortColumn))),
|
|
SortModeType.Descending => fa.OrderByDescending(x => x, new GridComparer(GetLambdaForValue(CurrentSortColumn))),
|
|
SortModeType.AscendingAbsolute => fa.OrderBy (x => x, new GridComparer(GetLambdaForAbsValue(CurrentSortColumn))),
|
|
SortModeType.DescendingAbsolute => fa.OrderByDescending(x => x, new GridComparer(GetLambdaForAbsValue(CurrentSortColumn))),
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
};
|
|
|
|
FilteredItems = [..fa];
|
|
|
|
try
|
|
{
|
|
await InvokeAsync(() => FilteredItemsChanged.InvokeAsync(FilteredItems));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
await UpdatePage(_currentPage);
|
|
}
|
|
#endregion
|
|
|
|
#region pagination
|
|
|
|
public async Task NavigateToPage(Direction direction)
|
|
{
|
|
var page = _currentPage;
|
|
switch (direction)
|
|
{
|
|
case Direction.Next:
|
|
{
|
|
if (page < _totalPages)
|
|
{
|
|
page += 1;
|
|
}
|
|
break;
|
|
}
|
|
case Direction.Previous:
|
|
{
|
|
if (page > 1)
|
|
{
|
|
page -= 1;
|
|
}
|
|
break;
|
|
}
|
|
case Direction.Back:
|
|
{
|
|
page = 1;
|
|
break;
|
|
}
|
|
case Direction.Forward:
|
|
{
|
|
page = _totalPages;
|
|
break;
|
|
}
|
|
}
|
|
await UpdatePage(page);
|
|
}
|
|
|
|
public async Task NavigateToPage(int page)
|
|
{
|
|
if (page < 1)
|
|
{
|
|
page = 1;
|
|
}
|
|
else if (page > _totalPages)
|
|
{
|
|
page = _totalPages;
|
|
}
|
|
|
|
await UpdatePage(page);
|
|
}
|
|
#endregion
|
|
|
|
List<TableColumnControl> Columns { get; set; } = [];
|
|
|
|
public void AddColumn(TableColumnControl column)
|
|
{
|
|
Columns.Add(column);
|
|
}
|
|
|
|
public void RemoveColumn(TableColumnControl column)
|
|
{
|
|
Columns.Remove(column);
|
|
}
|
|
|
|
public static string ConvertToString(object? value, string? format)
|
|
{
|
|
if (value == null)
|
|
return "";
|
|
|
|
if (value is string s)
|
|
return s;
|
|
|
|
if (format == null)
|
|
return value.ToString() ?? "";
|
|
|
|
|
|
return value switch
|
|
{
|
|
double d => d .ToString( format ),
|
|
float f => f .ToString( format ),
|
|
long l => l .ToString( format ),
|
|
int i => i .ToString( format ),
|
|
TimeSpan ts => ts .ToString( format == "N0" ? "g" : format),
|
|
decimal dc => dc .ToString( format ),
|
|
DateTime dt => DateTimeFormatting(dt, format),
|
|
_ => value.ToString() ?? ""
|
|
};
|
|
}
|
|
|
|
private static string DateTimeFormatting(DateTime dt, string format)
|
|
{
|
|
if ( format == "N2" )
|
|
{
|
|
return dt == DateTime.MinValue ? "" : dt.ToString("yyyy-MM-dd HH:mm:ss");
|
|
}
|
|
return format == "N1" && dt == DateTime.MinValue ? "" :
|
|
dt.ToString((format == "N0" || format == "N1") ? "yyyy-MM-dd" : format);
|
|
}
|
|
|
|
public static string DefaultGetCellClass(dynamic item, TableColumnControl column)
|
|
{
|
|
if (column?.Field == null)
|
|
return "";
|
|
|
|
var val = GetLambdaForValue(column.Field)(item);
|
|
return GetCellClass(item, val, column);
|
|
}
|
|
|
|
public static string GetCellClass(dynamic row, object? value, TableColumnControl column)
|
|
{
|
|
var c = column.Class;
|
|
|
|
var s = "";
|
|
var isZero = false;
|
|
var isNegative = false;
|
|
var isPositive = false;
|
|
var customStyle = "";
|
|
switch (value)
|
|
{
|
|
case double.NaN:
|
|
break;
|
|
case null:
|
|
break;
|
|
case double n:
|
|
isZero = n == 0.0;
|
|
isNegative = n < 0.0;
|
|
isPositive = n > 0.0;
|
|
break;
|
|
case int n:
|
|
isZero = n == 0.0;
|
|
isNegative = n < 0.0;
|
|
isPositive = n > 0.0;
|
|
break;
|
|
case long n:
|
|
isZero = n == 0L;
|
|
isNegative = n < 0L;
|
|
isPositive = n > 0L;
|
|
break;
|
|
case float n:
|
|
isZero = n == 0.0;
|
|
isNegative = n < 0.0;
|
|
isPositive = n > 0.0;
|
|
break;
|
|
case decimal d:
|
|
isZero = d == 0.0M;
|
|
isNegative = d < 0.0M;
|
|
isPositive = d > 0.0M;
|
|
break;
|
|
default: //string value
|
|
s = value.ToString();
|
|
break;
|
|
}
|
|
|
|
if (column.TableControl?.GetCellClassCallback != null)
|
|
{
|
|
customStyle = column.TableControl.GetCellClassCallback(row, column);
|
|
}
|
|
|
|
return (s?.Equals("") ?? false) switch
|
|
{
|
|
true when isZero => $"align-right {c} {column.ZeroValueClass} {customStyle}",
|
|
true when isNegative => $"align-right {c} {column.NegativeValueClass} {customStyle}",
|
|
true when isPositive => $"align-right {c} {column.PositiveValueClass} {customStyle}",
|
|
|
|
_ => $"align-left {c}{customStyle}"
|
|
};
|
|
}
|
|
|
|
private async Task OnSelectionChanged(dynamic selection, string? field)
|
|
{
|
|
SelectedItem = selection;
|
|
SelectedColumn = field;
|
|
|
|
if (Selectable)
|
|
{
|
|
await SelectionChanged.InvokeAsync((selection, field ?? ""));
|
|
}
|
|
}
|
|
|
|
public static object? GetDynamicMember(object obj, string memberName)
|
|
{
|
|
var binder = Binder.GetMember(CSharpBinderFlags.None, memberName, obj.GetType(),
|
|
[CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)]);
|
|
var callsite = CallSite<Func<CallSite, object, object>>.Create(binder);
|
|
try
|
|
{
|
|
return callsite.Target(callsite, obj);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void TriggerFilterRow()
|
|
{
|
|
Filterable = !Filterable;
|
|
}
|
|
|
|
private async Task OnCopyAllToClipboard()
|
|
{
|
|
string Shield(string s) => s.Contains(",") || s.Contains("\n") ? $"\"{s}\"" : s;
|
|
|
|
var str = new StringBuilder();
|
|
str.Append(string.Join(",", Columns.Select(col => Shield(col.Name ?? ""))));
|
|
str.AppendLine();
|
|
|
|
foreach (var row in FilteredItems)
|
|
{
|
|
str.Append(string.Join(",", Columns.Select(col =>
|
|
{
|
|
var colOriginalValue = GetLambdaForValue(col.Field)(row);
|
|
var colValue = ConvertToString(colOriginalValue, col.Format);
|
|
return Shield(colValue);
|
|
})));
|
|
str.AppendLine();
|
|
}
|
|
|
|
await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", str.ToString());
|
|
}
|
|
}
|