简体   繁体   中英

Reflection blazor - Get attribute name + value

On my parent component I have this:

<TestComponent @bind-Value="testString" />

@code {
    [MaxLength(10)]
    private string testString;
}

And on my TestComponent this:

<input type="text" @bind="Value" @bind:event="oninput" />

@code {
    [Parameter] public string Value { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }

    protected override void OnInitialized() {
        //Get MaxLength here
    }
}

How could I check in my TestComponent if Value have a MaxLength and get his value if it does?

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 now sucess.

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

eg

<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.

You can do this:

<TestComponent @bind-Value="@testString" />

@code {
    private string str;
    // Define testString as a parameter property

    [Parameter]
    public string testString 
    {
        get {return str;}
        set
        {
          if(str.Length <= 10) 
          {
             str = value;
          }
        }
    }
}

Note: What you needed here, is to define a public parameter property into which the value passed by ValueChanged.InvokeAsync is received. This is actually a true two-way component data binding...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM