AyCode.Blazor/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs

161 lines
5.3 KiB
C#

using DevExpress.Blazor;
using Microsoft.AspNetCore.Components;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
namespace AyCode.Blazor.Components.Components.Grids;
/// <summary>
/// Extended DxGridDataColumn with additional parameters for InfoPanel support.
/// </summary>
public partial class MgGridDataColumn : DxGridDataColumn
{
private static readonly ConcurrentDictionary<(Type Type, string Property), Func<object, object?>?> SAccessorCache = new();
private string? _urlLink;
private bool _isInitialized;
private TemplatePart[]? _templateParts;
/// <summary>
/// Whether this column should be visible in the InfoPanel. Default is true.
/// </summary>
[Parameter]
public bool ShowInInfoPanel { get; set; } = true;
/// <summary>
/// Custom display format for InfoPanel (overrides DisplayFormat if set).
/// </summary>
[Parameter]
public string? InfoPanelDisplayFormat { get; set; }
/// <summary>
/// Column order in InfoPanel (lower = earlier). Default is int.MaxValue.
/// </summary>
[Parameter]
public int InfoPanelOrder { get; set; } = int.MaxValue;
/// <summary>
/// URL template with {property} placeholders that will be replaced with row values.
/// Example: https://shop.fruitbank.hu/Admin/Order/Edit/{Id}/{OrderId}
/// </summary>
[Parameter]
public string? UrlLink
{
get => _urlLink;
set
{
if (_urlLink == value) return;
_urlLink = value;
if (_isInitialized) UpdateCellDisplayTemplate();
}
}
protected override void OnParametersSet()
{
base.OnParametersSet();
_isInitialized = true;
UpdateCellDisplayTemplate();
}
private void UpdateCellDisplayTemplate()
{
if (string.IsNullOrWhiteSpace(_urlLink)) return;
_templateParts = ParseTemplate(_urlLink);
var parts = _templateParts;
CellDisplayTemplate = context => builder =>
{
var url = BuildUrl(parts, context.DataItem);
builder.OpenElement(0, "a");
builder.AddAttribute(1, "href", url);
builder.AddAttribute(2, "target", "_blank");
builder.AddAttribute(3, "style", "text-decoration: underline; color: inherit;");
builder.AddContent(4, context.DisplayText);
builder.CloseElement();
};
}
/// <summary>
/// Represents a parsed segment of a URL template: either a literal string or a property placeholder.
/// </summary>
internal readonly record struct TemplatePart(string Value, bool IsProperty);
[GeneratedRegex(@"\{([^}]+)\}")]
private static partial Regex TemplateRegex();
/// <summary>
/// Parses a URL template into literal and property placeholder segments.
/// </summary>
internal static TemplatePart[] ParseTemplate(string template)
{
var parts = new List<TemplatePart>();
var lastIndex = 0;
foreach (Match match in TemplateRegex().Matches(template))
{
if (match.Index > lastIndex) parts.Add(new TemplatePart(template[lastIndex..match.Index], IsProperty: false));
parts.Add(new TemplatePart(match.Groups[1].Value, IsProperty: true));
lastIndex = match.Index + match.Length;
}
if (lastIndex < template.Length) parts.Add(new TemplatePart(template[lastIndex..], IsProperty: false));
return [.. parts];
}
/// <summary>
/// Builds a URL from pre-parsed template parts using cached compiled property accessors.
/// </summary>
internal static string BuildUrl(TemplatePart[] parts, object? dataItem)
{
if (dataItem is null || parts.Length == 0)
return string.Empty;
var type = dataItem.GetType();
var sb = new StringBuilder(parts.Length * 16);
foreach (var part in parts)
{
if (!part.IsProperty)
{
sb.Append(part.Value);
continue;
}
var accessor = SAccessorCache.GetOrAdd((type, part.Value), static key => CompileAccessor(key.Type, key.Property));
if (accessor is not null) sb.Append(accessor(dataItem)?.ToString() ?? string.Empty);
else sb.Append('{').Append(part.Value).Append('}');
}
return sb.ToString();
}
/// <summary>
/// Replaces {property} placeholders in the template with values from the data item.
/// Convenience overload that parses the template on each call — prefer pre-parsed <see cref="BuildUrl"/> for hot paths.
/// </summary>
internal static string BuildUrlFromTemplate(string template, object? dataItem)
{
return dataItem is null ? template : BuildUrl(ParseTemplate(template), dataItem);
}
private static Func<object, object?>? CompileAccessor(Type type, string propertyName)
{
var prop = type.GetProperty(propertyName);
if (prop is null) return null;
var param = Expression.Parameter(typeof(object), "obj");
var body = Expression.Convert(
Expression.Property(Expression.Convert(param, type), prop),
typeof(object));
return Expression.Lambda<Func<object, object?>>(body, param).Compile();
}
}