I already have this need to check for attributes to use DisplayName
to render a label and Required
to add a red *
to the label, and I didn't know how to do this, I tried the normal way of doing it, but no success.
But then I remembered that Blazor already does this with ValidationMessage
because it gets the attributes from a property and validates it... so I decided to check the source code of it.
Digging very deep, I found this function which explains how to do what we need.
First of all, it have a Expression<Func<T>>
parameter, which in blazor is the For
property of ValidationMessage
, so we can see here that probably isn't possible to do it with a binded value or just passing it like Foo="@Foo"
(if it was possible, they would probably have done that way), so we need to have another property that will pass that type
e.g.
<TestComponent @bind-Value="testString" Field=@"(() => testString)" />
Now continuing with the code from that function, it will get the Body
of the expression and do some checkings to get and make sure you are passing a property.
And then there is this line.
fieldName = memberExpression.Member.Name;
And if you take a look at memberExpression.Member
and call GetCustomAttributes
you will have exactly what we need, all the custom attributes of the proprety.
So now all we need is just loop the custom attributes and do what ever you want.
Here is a simplified version to get the CustomAttribute
of the property returned in the Expression<Func<T>>
private IEnumerable<CustomAttributeData> GetExpressionCustomAttributes<T>(Expression<Func<T>> accessor)
{
var accessorBody = accessor.Body;
// Unwrap casts to object
if (accessorBody is UnaryExpression unaryExpression
&& unaryExpression.NodeType == ExpressionType.Convert
&& unaryExpression.Type == typeof(object))
{
accessorBody = unaryExpression.Operand;
}
if (!(accessorBody is MemberExpression memberExpression))
{
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
}
return memberExpression.Member.GetCustomAttributes();
}
For you example, here is how to solve it
.razor
<TestComponent @bind-Value="testString" Field="(() => testString)" />
@code {
[MaxLength(10)]
private string testString;
}
TestComponent.razor
<input type="text" @bind="Value" @bind:event="oninput" />
@code {
[Parameter] public Expression<Func<string>>Field { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
if (Field != null)
{
var attrs = GetExpressionCustomAttributes(Field);
foreach (var attr in attrs)
{
if(attr is MaxLengthAttribute maxLengthAttribute)
{
// Do what you want with maxLengthAttribute
}
}
}
}
private IEnumerable<CustomAttributeData> GetExpressionCustomAttributes<T>(Expression<Func<T>> accessor)
{
var accessorBody = accessor.Body;
// Unwrap casts to object
if (accessorBody is UnaryExpression unaryExpression
&& unaryExpression.NodeType == ExpressionType.Convert
&& unaryExpression.Type == typeof(object))
{
accessorBody = unaryExpression.Operand;
}
if (!(accessorBody is MemberExpression memberExpression))
{
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
}
return memberExpression.Member.GetCustomAttributes();
}
}
If you only have one attribute, you can also call memberExpression.Member.GetCustomAttributes<Attribute>()
to get a list of that attribute type.
TL;DR
Add a new property to the component
[Parameter] public Expression<Func<T>>Field { get; set; }
Use this gist helper functions to get the attribute(s) you want.