diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs b/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs index 540194f..630b6f2 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridDataColumn.cs @@ -1,6 +1,8 @@ using DevExpress.Blazor; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Text; using System.Text.RegularExpressions; namespace AyCode.Blazor.Components.Components.Grids; @@ -8,10 +10,13 @@ namespace AyCode.Blazor.Components.Components.Grids; /// /// Extended DxGridDataColumn with additional parameters for InfoPanel support. /// -public class MgGridDataColumn : DxGridDataColumn +public partial class MgGridDataColumn : DxGridDataColumn { + private static readonly ConcurrentDictionary<(Type Type, string Property), Func?> SAccessorCache = new(); + private string? _urlLink; private bool _isInitialized; + private TemplatePart[]? _templateParts; /// /// Whether this column should be visible in the InfoPanel. Default is true. @@ -42,6 +47,7 @@ public class MgGridDataColumn : DxGridDataColumn set { if (_urlLink == value) return; + _urlLink = value; if (_isInitialized) UpdateCellDisplayTemplate(); } @@ -56,41 +62,99 @@ public class MgGridDataColumn : DxGridDataColumn private void UpdateCellDisplayTemplate() { - if (!string.IsNullOrWhiteSpace(_urlLink)) + if (string.IsNullOrWhiteSpace(_urlLink)) return; + + _templateParts = ParseTemplate(_urlLink); + var parts = _templateParts; + + CellDisplayTemplate = context => builder => { - CellDisplayTemplate = context => builder => - { - var url = BuildUrlFromTemplate(_urlLink, 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(); - }; + 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(); + }; + } + + /// + /// Represents a parsed segment of a URL template: either a literal string or a property placeholder. + /// + internal readonly record struct TemplatePart(string Value, bool IsProperty); + + [GeneratedRegex(@"\{([^}]+)\}")] + private static partial Regex TemplateRegex(); + + /// + /// Parses a URL template into literal and property placeholder segments. + /// + internal static TemplatePart[] ParseTemplate(string template) + { + var parts = new List(); + 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]; + } + + /// + /// Builds a URL from pre-parsed template parts using cached compiled property accessors. + /// + 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(); } /// /// Replaces {property} placeholders in the template with values from the data item. - /// Exposed for unit testing. + /// Convenience overload that parses the template on each call — prefer pre-parsed for hot paths. /// internal static string BuildUrlFromTemplate(string template, object? dataItem) { - if (dataItem == null) return template; + return dataItem is null ? template : BuildUrl(ParseTemplate(template), dataItem); + } - return Regex.Replace(template, "{([^}]+)}", match => - { - var propName = match.Groups[1].Value; + private static Func? CompileAccessor(Type type, string propertyName) + { + var prop = type.GetProperty(propertyName); + if (prop is null) return null; - //TODO: delegate-et kéne használni és cache-elni egy dictionary-ba! - J. - var prop = dataItem.GetType().GetProperty(propName); - if (prop != null) - { - var value = prop.GetValue(dataItem); - return value?.ToString() ?? string.Empty; - } - return match.Value; - }); + var param = Expression.Parameter(typeof(object), "obj"); + var body = Expression.Convert( + Expression.Property(Expression.Convert(param, type), prop), + typeof(object)); + + return Expression.Lambda>(body, param).Compile(); } }