How to make the asp-for input tag helper generate camelCase names?
Asked Answered
M

2

6

If I have a view model like this:

 public class MyModel{
      public DateTime? StartDate {get;set;}
 }

And on a view an input tag is used with an asp-for tag helper like so:

<input asp-for="StartDate" />

The default html that is generated by this is

 <input type="datetime" id="StartDate" name="StartDate" value="" />

But what I want it to generate is html that looks like this:

 <input type="datetime" id="startDate" name="startDate" value="" />

How can I make the asp-for input tag helper generate camel case names like above without having to make my model properties camelCase?

Mady answered 11/4, 2017 at 22:10 Comment(2)
Do you really care? If the taghelper generates name="StartDate" but you want your controller method to use the name startDate, the model binder handles that for you.Pleochroism
I do care. Because I may access that field with client side JavaScript via its id and I want that code to follow the coding conventions of my codebase.Mady
M
9

After studying the code that @Bebben posted and the link provided with it, I continued to dig more into the Asp.Net Core source code. And I found that the designers of the Asp.Net Core provided some extensibility points that could be leveraged to achieve lower camelCase id and name values.

To do it, we need to implement our own IHtmlGenerator which we can do by creating a custom class that inherits from DefaultHtmlGenerator. Then on that class we need to override the GenerateTextBox method to fix the casing. Or alternatively we can override the GenerateInput method to fix the casing of name and id attribute values for all input fields (not just input text fields) which is what I chose to do. As a bonus I also override the GenerateLabel method so the label's for attribute also specifies a value using the custom casing.

Here's the class:

    using Microsoft.AspNetCore.Antiforgery;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Internal;
    using Microsoft.AspNetCore.Mvc.ModelBinding;
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Mvc.Routing;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    using Microsoft.Extensions.Options;
    using System.Collections.Generic;
    using System.Text.Encodings.Web;

    namespace App.Web {
        public class CustomHtmlGenerator : DefaultHtmlGenerator {

            public CustomHtmlGenerator(
                IAntiforgery antiforgery,
                IOptions<MvcViewOptions> optionsAccessor,
                IModelMetadataProvider metadataProvider,
                IUrlHelperFactory urlHelperFactory,
                HtmlEncoder htmlEncoder,
                ClientValidatorCache clientValidatorCache) : base
                                (antiforgery, optionsAccessor, metadataProvider, urlHelperFactory,
                                htmlEncoder, clientValidatorCache) {

               //Nothing to do

            }

            public CustomHtmlGenerator(
                IAntiforgery antiforgery,
                IOptions<MvcViewOptions> optionsAccessor,
                IModelMetadataProvider metadataProvider,
                IUrlHelperFactory urlHelperFactory,
                HtmlEncoder htmlEncoder,
                ClientValidatorCache clientValidatorCache,
                ValidationHtmlAttributeProvider validationAttributeProvider) : base
                                (antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder,
                                clientValidatorCache, validationAttributeProvider) {

                //Nothing to do

            }


            protected override TagBuilder GenerateInput(
                ViewContext viewContext,
                InputType inputType,
                ModelExplorer modelExplorer,
                string expression,
                object value,
                bool useViewData,
                bool isChecked,
                bool setId,
                bool isExplicitValue,
                string format,
                IDictionary<string, object> htmlAttributes) {

                expression = GetLowerCamelCase(expression);

                return base.GenerateInput(viewContext, inputType, modelExplorer, expression, value, useViewData, 
                                        isChecked, setId, isExplicitValue, format, htmlAttributes);
            }


            public override TagBuilder GenerateLabel(
                ViewContext viewContext,
                ModelExplorer modelExplorer,
                string expression,
                string labelText,
                object htmlAttributes) {

                expression = GetLowerCamelCase(expression);

                return base.GenerateLabel(viewContext, modelExplorer, expression, labelText, htmlAttributes);
            }


            private string GetLowerCamelCase(string text) {

                if (!string.IsNullOrEmpty(text)) {
                    if (char.IsUpper(text[0])) {
                        return char.ToLower(text[0]) + text.Substring(1);
                    }
                }

                return text;
            }

        }
    }

Now that we have our CustomHtmlGenerator class we need to register it in the IoC container in place of the DefaultHtmlGenerator. We can do that in the ConfigureServices method of the Startup.cs via the following two lines:

  //Replace DefaultHtmlGenerator with CustomHtmlGenerator
  services.Remove<IHtmlGenerator, DefaultHtmlGenerator>();
  services.AddTransient<IHtmlGenerator, CustomHtmlGenerator>();

Pretty cool. And not only have we solved the id and name casing issue on the input fields but by implementing our own custom IHtmlGenerator, and getting it registered, we have opened the door on all kinds of html customization that can be done.

I'm starting to really appreciate the power of a system built around an IoC, and default classes with virtual methods. The level of customization available with little effort under such an approach is really pretty amazing.

Update
@Gup3rSuR4c pointed out that my services.Remove call must be an extension method that's not included in the framework. I checked, and yep that true. So, here is the code for that extension method:

 public static class IServiceCollectionExtensions {

    public static void Remove<TServiceType, TImplementationType>(this IServiceCollection services) {

        var serviceDescriptor = services.First(s => s.ServiceType == typeof(TServiceType) &&
                                                    s.ImplementationType == typeof(TImplementationType));
        services.Remove(serviceDescriptor); 
    }

}
Mady answered 13/4, 2017 at 18:56 Comment(4)
Your answer led me to what I needed which was to replace the darn underscore that gets generated for ids with a dot. It should be noted though that the services.Remove<TService, TImplementation>() you're using is not a built in extension and has to be created separately, or at least as far as I know that seems to be the case.Beguine
Thanks for your comment. Good point. I will update my answer with the code for that services.Remove Extension method.Mady
Using an airplane to plow a field...? Isn't this a "little" bit too much of an overhead...?Silicle
@MladenB.Should be almost no overhead, only the overhead of the string manipulation to lowercase a single leading char when needed.Mady
I
6

The simplest way to do this is to just write

<input asp-for="StartDate" name="startDate" />

Or do you want to have it generated completely automatically in camel case, for the whole application?

To do that, it seems like you have to implement your own InputTagHelpers in Microsoft.AspNetCore.Mvc.TagHelpers.

Here is the method where the name is generated:

private TagBuilder GenerateTextBox(ModelExplorer modelExplorer, string inputTypeHint, string inputType)
{
    var format = Format;
    if (string.IsNullOrEmpty(format))
    {
        format = GetFormat(modelExplorer, inputTypeHint, inputType);
    }

    var htmlAttributes = new Dictionary<string, object>
    {
        { "type", inputType }
    };

    if (string.Equals(inputType, "file") && string.Equals(inputTypeHint, TemplateRenderer.IEnumerableOfIFormFileName))
    {
        htmlAttributes["multiple"] = "multiple";
    }

    return Generator.GenerateTextBox(
        ViewContext,
        modelExplorer,
        For.Name,
        value: modelExplorer.Model,
        format: format,
        htmlAttributes: htmlAttributes);
}

(The above code is from https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs, Apache License, Version 2.0, Copyright .NET Foundation)

The line is "For.Name". The name is sent into some other methods, and the one that in the end gives the final name is in a static class (Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.NameAndIdProvider), so nothing we can really plug into easily.

Inexertion answered 13/4, 2017 at 15:44 Comment(2)
Your answer inspired me to dig deeper into the source code and I found that it is possible to implement a system wide approach. For the first time I'm truly starting to appreciate the power of a system build using and IoC container.Mady
@Mady That is great news :-)Inexertion

© 2022 - 2024 — McMap. All rights reserved.