Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions src/Components/Web/src/Forms/DisplayName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;

namespace Microsoft.AspNetCore.Components.Forms;

/// <summary>
/// Displays the display name for a specified field, reading from <see cref="DisplayAttribute"/>
/// or <see cref="DisplayNameAttribute"/> if present, or falling back to the property name.
/// </summary>
/// <typeparam name="TValue">The type of the field.</typeparam>
public class DisplayName<TValue> : IComponent
{
private static readonly ConcurrentDictionary<MemberInfo, string> _displayNameCache = new();

private RenderHandle _renderHandle;
private Expression<Func<TValue>>? _previousFieldAccessor;
private string? _displayName;

/// <summary>
/// Specifies the field for which the display name should be shown.
/// </summary>
[Parameter, EditorRequired]
public Expression<Func<TValue>>? For { get; set; }

static DisplayName()
{
if (HotReloadManager.Default.MetadataUpdateSupported)
{
HotReloadManager.Default.OnDeltaApplied += ClearCache;
}
}

/// <inheritdoc />
void IComponent.Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}

/// <inheritdoc />
Task IComponent.SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);

if (For is null)
{
throw new InvalidOperationException($"{GetType()} requires a value for the " +
$"{nameof(For)} parameter.");
}

// Only recalculate if the expression changed
if (For != _previousFieldAccessor)
{
var member = ExpressionMemberAccessor.GetMemberInfo(For);
var newDisplayName = GetDisplayName(member);

if (newDisplayName != _displayName)
{
_displayName = newDisplayName;
_renderHandle.Render(BuildRenderTree);
}

_previousFieldAccessor = For;
}

return Task.CompletedTask;
}

private void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, _displayName);
}

private static string GetDisplayName(MemberInfo member)
{
return _displayNameCache.GetOrAdd(member, static m =>
{
var displayAttribute = m.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute is not null)
{
var name = displayAttribute.GetName();
if (name is not null)
{
return name;
}
}

var displayNameAttribute = m.GetCustomAttribute<DisplayNameAttribute>();
if (displayNameAttribute?.DisplayName is not null)
{
return displayNameAttribute.DisplayName;
}

return m.Name;
});
}

private static void ClearCache()
{
_displayNameCache.Clear();
}
}
54 changes: 54 additions & 0 deletions src/Components/Web/src/Forms/ExpressionMemberAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Components.HotReload;

namespace Microsoft.AspNetCore.Components.Forms;

internal static class ExpressionMemberAccessor
{
private static readonly ConcurrentDictionary<Expression, MemberInfo> _memberInfoCache = new();

static ExpressionMemberAccessor()
{
if (HotReloadManager.Default.MetadataUpdateSupported)
{
HotReloadManager.Default.OnDeltaApplied += ClearCache;
}
}

public static MemberInfo GetMemberInfo<TValue>(Expression<Func<TValue>> accessor)
{
ArgumentNullException.ThrowIfNull(accessor);

return _memberInfoCache.GetOrAdd(accessor, static expr =>
{
var lambdaExpression = (LambdaExpression)expr;
var accessorBody = lambdaExpression.Body;

if (accessorBody is UnaryExpression unaryExpression
&& unaryExpression.NodeType == ExpressionType.Convert
&& unaryExpression.Type == typeof(object))
{
accessorBody = unaryExpression.Operand;
}

if (accessorBody is not MemberExpression memberExpression)
{
throw new ArgumentException(
$"The provided expression contains a {accessorBody.GetType().Name} which is not supported. " +
$"Only simple member accessors (fields, properties) of an object are supported.");
}

return memberExpression.Member;
});
}

private static void ClearCache()
{
_memberInfoCache.Clear();
}
}
4 changes: 4 additions & 0 deletions src/Components/Web/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ Microsoft.AspNetCore.Components.Web.Media.MediaSource.MediaSource(byte[]! data,
Microsoft.AspNetCore.Components.Web.Media.MediaSource.MediaSource(System.IO.Stream! stream, string! mimeType, string! cacheKey) -> void
Microsoft.AspNetCore.Components.Web.Media.MediaSource.MimeType.get -> string!
Microsoft.AspNetCore.Components.Web.Media.MediaSource.Stream.get -> System.IO.Stream!
Microsoft.AspNetCore.Components.Forms.DisplayName<TValue>
Microsoft.AspNetCore.Components.Forms.DisplayName<TValue>.DisplayName() -> void
Microsoft.AspNetCore.Components.Forms.DisplayName<TValue>.For.get -> System.Linq.Expressions.Expression<System.Func<TValue>!>?
Microsoft.AspNetCore.Components.Forms.DisplayName<TValue>.For.set -> void
Loading
Loading