I'm trying to simplify creation of long forms with the following custom tag helper in .net Core 2.0:
@model ObApp.Web.Models.ViewComponents.FormFieldViewModel
<span>Debug - Paramater value: @Model.FieldFor</span>
<div class="form-group">
<label asp-for="@Model.FieldFor"></label>
<input asp-for="@Model.FieldFor" class="form-control" />
</div>
It seems simple, but I'm getting an unexpected (to me) result when I use:
<vc:form-field field-for="PersonEmail"></vc:form-field>
Expected Result
<span>Debug - Paramater value: PersonEmail</span>
<div class="form-group">
<label for="PersonEmail">Email</label>
<input name="PersonEmail" class="form-control" id="PersonEmail"
type="text" value="PersonEmail">
</div>
Actual Result
<span>Debug - Paramater value: PersonEmail</span>
<div class="form-group">
<label for="FieldFor">FieldFor</label>
<input name="FieldFor" class="form-control" id="FieldFor"
type="text" value="PersonEmail">
</div>
I've tried removing the quotes from around @Model.FieldFor, as well as a few other syntactic changes.
Any suggestions?
Thank you!
Tag Helpers are attached to HTML elements inside your Razor views and can help you write markup that is both cleaner and easier to read than the traditional HTML Helpers. HTML Helpers, on the other hand, are invoked as methods that are mixed with HTML inside your Razor views.
The Input Tag Helper. The Input Tag Helper binds an HTML <input> element to a model expression in your razor view. The Input Tag Helper: Generates the id and name HTML attributes for the expression name specified in the asp-for attribute.
No, tag helpers are only supported in ASP.NET Core, not in the older ASP.NET (based on the classic . NET Framework, rather than the newer . NET Core). But instead you can use HTML Helpers which do a lot of the same things.
@addTagHelper makes Tag Helpers available The code above uses the wildcard syntax ("*") to specify that all Tag Helpers in the specified assembly (Microsoft. AspNetCore. Mvc. TagHelpers) will be available to every view file in the Views directory or subdirectory.
As was pointed out to me by others, it may not be possible to directly embed tag helpers the way I first desired when I posted this question. As a result I refactored the code to programmatically "new up" the desired tag helpers instead.
My final solution was significantly more work than I had expected but in the long run it will save lots of time developing the form-intensive applications I have planned.
The Objective
My goal is to speed-up creation of forms by using this custom tag helper, for example:
<formfield asp-for="OrganizationName"></formfield>
To generate these built-in Razor tag helpers:
<div class="form-group">
<div class="row">
<label class="col-md-3 col-form-label" for="OrganizationName">Company Name</label>
<div class="col-md-9">
<input name="OrganizationName" class="form-control" id="OrganizationName" type="text" value="" data-val-required="The Company Name field is required." data-val="true" data-val-maxlength-max="50" data-val-maxlength="Maximum company name length is 50 characters.">
<span class="field-validation-valid" data-valmsg-replace="true" data-valmsg-for="OrganizationName"></span>
</div>
</div>
</div>
My Initial Working Solution
This is the first-pass test solution for simple cases. I.e. for default hard coded classes and text-box input types.
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ObApp.Web.TagHelpers
{
// Builds form elements to generate the following (for example):
// <div class="form-group">
// <div class="row">
// <input ... >Email</input>
// <div>
// <input type="text" ... />
// <span class="field-validation-valid ... ></span>
// </div>
// </div>
// </div>
public class FormfieldTagHelper : TagHelper
{
private const string _forAttributeName = "asp-for";
private const string _defaultWraperDivClass = "form-group";
private const string _defaultRowDivClass = "row";
private const string _defaultLabelClass = "col-md-3 col-form-label";
private const string _defaultInputClass = "form-control";
private const string _defaultInnerDivClass = "col-md-9";
private const string _defaultValidationMessageClass = "";
public FormfieldTagHelper(IHtmlGenerator generator)
{
Generator = generator;
}
[HtmlAttributeName(_forAttributeName)]
public ModelExpression For { get; set; }
public IHtmlGenerator Generator { get; }
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// Replace this parent tag helper with div tags wrapping the entire form block
output.TagName = "div";
output.Attributes.SetAttribute("class", _defaultWraperDivClass);
// Manually new-up each child asp form tag helper element
TagHelperOutput labelElement = await CreateLabelElement(context);
TagHelperOutput inputElement = await CreateInputElement(context);
TagHelperOutput validationMessageElement = await CreateValidationMessageElement(context);
// Wrap input and validation with column div
IHtmlContent innerDiv = WrapElementsWithDiv(
new List<IHtmlContent>()
{
inputElement,
validationMessageElement
},
_defaultInnerDivClass
);
// Wrap all elements with a row div
IHtmlContent rowDiv = WrapElementsWithDiv(
new List<IHtmlContent>()
{
labelElement,
innerDiv
},
_defaultRowDivClass
);
// Put everything into the innerHtml of this tag helper
output.Content.SetHtmlContent(rowDiv);
}
private async Task<TagHelperOutput> CreateLabelElement(TagHelperContext context)
{
LabelTagHelper labelTagHelper =
new LabelTagHelper(Generator)
{
For = this.For,
ViewContext = this.ViewContext
};
TagHelperOutput labelOutput = CreateTagHelperOutput("label");
await labelTagHelper.ProcessAsync(context, labelOutput);
labelOutput.Attributes.Add(
new TagHelperAttribute("class", _defaultLabelClass));
return labelOutput;
}
private async Task<TagHelperOutput> CreateInputElement(TagHelperContext context)
{
InputTagHelper inputTagHelper =
new InputTagHelper(Generator)
{
For = this.For,
ViewContext = this.ViewContext
};
TagHelperOutput inputOutput = CreateTagHelperOutput("input");
await inputTagHelper.ProcessAsync(context, inputOutput);
inputOutput.Attributes.Add(
new TagHelperAttribute("class", _defaultInputClass));
return inputOutput;
}
private async Task<TagHelperOutput> CreateValidationMessageElement(TagHelperContext context)
{
ValidationMessageTagHelper validationMessageTagHelper =
new ValidationMessageTagHelper(Generator)
{
For = this.For,
ViewContext = this.ViewContext
};
TagHelperOutput validationMessageOutput = CreateTagHelperOutput("span");
await validationMessageTagHelper.ProcessAsync(context, validationMessageOutput);
return validationMessageOutput;
}
private IHtmlContent WrapElementsWithDiv(List<IHtmlContent> elements, string classValue)
{
TagBuilder div = new TagBuilder("div");
div.AddCssClass(classValue);
foreach(IHtmlContent element in elements)
{
div.InnerHtml.AppendHtml(element);
}
return div;
}
private TagHelperOutput CreateTagHelperOutput(string tagName)
{
return new TagHelperOutput(
tagName: tagName,
attributes: new TagHelperAttributeList(),
getChildContentAsync: (s, t) =>
{
return Task.Factory.StartNew<TagHelperContent>(
() => new DefaultTagHelperContent());
}
);
}
}
}
Next Steps/Suggested Improvements
This is working well for text boxes without validation errors. After tweaking the default CSS the next steps I plan to take are:
Thank you to @Chris Pratt for getting me started in the right direction on this.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With