Search code examples
c#asp.net-corerazorattributes

Extending IHtmlHelper - checking property's DataTypeAttribute in MyEditorFor


I'm making my own Html.EditorFor method, where I want to check the DataTypeAttribute of the given property. It's easy getting the type of the ViewData.Model. But if the property comes from a different class, like in the sample usage line, I can't find a way other than passing the type of it's container class as an argument.

How can I get the DataTypeAttribute of the property while omitting the containerType argument?

This is my current code:

public static IHtmlContent MyEditorFor<TModel, TResult>(this IHtmlHelper<TModel> html,
    Expression<Func<TModel, TResult>> expression,
    object moreData,
    Type? containerType = null)
{
    var ep = html.ViewContext.HttpContext.RequestServices.GetService(typeof(ModelExpressionProvider)) as ModelExpressionProvider;
    var bindName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ep.GetExpressionText(expression));

    if (containerType == null) containerType = html.ViewData.Model.GetType();
    var dataType = GetDataType(containerType, bindName);

    //etc...
}

private static DataType GetDataType(Type containerType, string bindName)
{
    var result = DataType.Text;
    var propName = bindName.Split('.')[^1];
    var prop = containerType.GetProperty(propName);
    var att = prop.GetCustomAttributes(typeof(DataTypeAttribute), true);
    var attProperty = typeof(DataTypeAttribute).GetProperty(nameof(DataTypeAttribute.DataType));
    result = (DataType)(attProperty.GetValue(att[0]) ?? DataType.Text);
    return result;
}

And here is an example usage:

@Html.EditorForEx(x => x.LinkedTable[i].MyDate, new { placeholder = "Test Date" }, Model.LinkedTable[i].GetType())
  • Null checks are omitted for simplicity

Solution

  • You should change your HtmlHelperExtensions like below.

    using System;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.Html;
    using Microsoft.AspNetCore.Mvc.Rendering;
    
    namespace _79271231.Helpers
    {
        public static class HtmlHelperExtensions
        {
            public static IHtmlContent MyEditorFor<TModel, TResult>(
                this IHtmlHelper<TModel> html,
                Expression<Func<TModel, TResult>> expression,
                object moreData)
            {
                var expressionText = GetExpressionText(expression);
                var fullName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText);
    
                var (containerType, propertyInfo) = GetContainerTypeAndProperty(expression);
    
                var dataType = GetDataType(containerType, propertyInfo);
    
                switch (dataType)
                {
                    case DataType.Date:
                        return html.TextBoxFor(expression, new { type = "date", @class = "form-control", placeholder = GetPlaceholder(moreData) });
                    case DataType.MultilineText:
                        return html.TextAreaFor(expression, new { @class = "form-control", placeholder = GetPlaceholder(moreData) });
                    default:
                        return html.TextBoxFor(expression, new { @class = "form-control", placeholder = GetPlaceholder(moreData) });
                }
            }
    
            private static string GetExpressionText<TModel, TResult>(Expression<Func<TModel, TResult>> expression)
            {
                var memberNames = new System.Collections.Generic.List<string>();
                Expression expr = expression.Body;
    
                while (expr is MemberExpression memberExpr)
                {
                    memberNames.Insert(0, memberExpr.Member.Name);
                    expr = memberExpr.Expression;
                }
    
                if (expr is MethodCallExpression methodCallExpr && methodCallExpr.Arguments.Count > 0)
                {
                    var methodName = methodCallExpr.Method.Name;
                    if (methodName == "get_Item" && methodCallExpr.Arguments[0] is ConstantExpression constExpr)
                    {
                        memberNames.Insert(1, $"[{constExpr.Value}]");
                    }
                    else
                    {
                        memberNames.Insert(1, "[*]");
                    }
                }
    
                return string.Join(".", memberNames);
            }
    
            private static (Type containerType, PropertyInfo propertyInfo) GetContainerTypeAndProperty<TModel, TResult>(
                Expression<Func<TModel, TResult>> expression)
            {
                MemberExpression memberExpr = null;
    
                if (expression.Body is MemberExpression)
                {
                    memberExpr = (MemberExpression)expression.Body;
                }
                else if (expression.Body is UnaryExpression unaryExpr && unaryExpr.Operand is MemberExpression)
                {
                    memberExpr = (MemberExpression)unaryExpr.Operand;
                }
    
                if (memberExpr == null)
                    throw new ArgumentException("Expression is not a member access", nameof(expression));
    
                var propertyInfos = new System.Collections.Generic.List<PropertyInfo>();
                var type = typeof(TModel);
                Expression expr = memberExpr;
    
                while (expr is MemberExpression m)
                {
                    if (m.Member is PropertyInfo pi)
                    {
                        propertyInfos.Insert(0, pi);
                        type = pi.PropertyType;
                        expr = m.Expression;
                    }
                    else
                    {
                        throw new ArgumentException("Member is not a property", nameof(expression));
                    }
                }
    
                if (propertyInfos.Count == 0)
                    throw new ArgumentException("No properties found in expression", nameof(expression));
    
                PropertyInfo propertyInfoFinal = propertyInfos[^1];
                Type containerTypeFinal = typeof(TModel);
    
                if (propertyInfos.Count > 1)
                {
                    PropertyInfo containerProperty = propertyInfos[^2];
                    containerTypeFinal = containerProperty.PropertyType;
                }
    
                return (containerTypeFinal, propertyInfoFinal);
            }
    
            private static DataType GetDataType(Type containerType, PropertyInfo propertyInfo)
            {
                var att = propertyInfo.GetCustomAttribute<DataTypeAttribute>();
                return att?.DataType ?? DataType.Text;
            }
    
            private static string GetPlaceholder(object moreData)
            {
                if (moreData == null) return string.Empty;
                var placeholderProperty = moreData.GetType().GetProperty("placeholder");
                return placeholderProperty?.GetValue(moreData)?.ToString() ?? string.Empty;
            }
        }
    }
    

    And use it like below, it works for me.

    @model _79271231.Models.SampleModel
    @using _79271231.Helpers
    
    @{
        ViewData["Title"] = "Custom EditorFor Sample";
    }
    
    <h1>@ViewData["Title"]</h1>
    
    <form asp-action="Index" method="post">
        <table class="table">
            <thead>
                <tr>
                    <th>Date</th>
                    <th>Description</th>
                </tr>
            </thead>
            <tbody>
                @for (int i = 0; i < Model.LinkedTable.Length; i++)
                {
                    <tr>
                        <td>
                            @Html.MyEditorFor(x => x.LinkedTable[i].MyDate, new { placeholder = "Test Date" })
                        </td>
                        <td>
                            @Html.MyEditorFor(x => x.LinkedTable[i].Description, new { placeholder = "Test Description" })
                        </td>
                    </tr>
                }
            </tbody>
        </table>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>