0

I want to make a partial view for form fields, so I don't have to write ~10 lines of boilerplate code everytime.

I tried the following:

FieldViewModel.cs

using Microsoft.AspNetCore.Mvc.ViewFeatures;

public class FieldViewModel
{
    public FieldViewModel(ModelExpression Data, string Label = "")
    {
        this.Data = Data;
        this.Label = Label;
    }

    public ModelExpression Data { get; set; }

    public string Label { get; set; }
}

_Field.cshtml (I stripped out the html code for this example)

// ...
<input asp-for="@Model.Data" />
// ...

and then I call it like this:

@await Html.PartialAsync("_Field", new FieldViewModel(Model.FirstName, "First Name"))

Obviously, this doesn't work because we pass string and it expects ModelExpression. Is there any way to make work? Maybe build somehow the ModelExpression manually (not sure how) and then pass it?

5
  • I am not sure if that helps, but this method called from the view gives back ModelExpression: this.ModelExpressionProvider.CreateModelExpression<(ModelType), (ValueType)>(this.ViewData, x => x.(Property)). Commented Jan 29, 2022 at 20:24
  • @spzvtbg Interesting solution but still doesn't work properly. Now the <input> still has name="Data" and Microsoft.AspNetCore.Mvc.ViewFeatures.ModelExpression as the value of the rendered field. Commented Jan 29, 2022 at 20:59
  • well that is not how the model expressions working, they are build into the tag helpers from expressions that accesing some model property, you cann't provode them as modelexpression because they will be wrappt in another model expession Commented Jan 29, 2022 at 23:09
  • they also containing all of the model an prperty data, so you have to get the value, name, id, error and what you need for properly build html input. that is actual what asp do for you beheind Commented Jan 29, 2022 at 23:13
  • @spzvtbg So, there's no way to make a reusable form field component? Commented Jan 30, 2022 at 10:25

1 Answer 1

1

I've tried something and it works so far, maybe there are still some edge cases that you have to consider and some more TagHelpers that you should extend, but basically it looks good.

First I have a few taghelpers extended, for label input and span.

using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

using System.Threading.Tasks;

In the following cases, "asp-bind" is the name of the attribute that accepts the ModelExpression and reassigns it to the "asp-for" property.

[HtmlTargetElement("label", Attributes = "asp-bind")]
public class FormGroupLabelTagHelper : LabelTagHelper
{
    public FormGroupLabelTagHelper(IHtmlGenerator generator)
        : base(generator) { }

    [HtmlAttributeName("asp-bind")]
    public ModelExpression AspBind
    {
        get => base.For;
        set => base.For = value.Model as ModelExpression;
    }

    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        => base.ProcessAsync(context, output);
}

...

[HtmlTargetElement("input", Attributes = "asp-bind")]
public class FormGroupInputTagHelper : InputTagHelper
{
    public FormGroupInputTagHelper(IHtmlGenerator generator)
        : base(generator) { }

    [HtmlAttributeName("asp-bind")]
    public ModelExpression AspBind
    {
        get => base.For;
        set => base.For = value.Model as ModelExpression;
    }

    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 
        => base.ProcessAsync(context, output);
}

...

[HtmlTargetElement("span", Attributes = "asp-bind")]
public class FormGroupErrorTagHelper : ValidationMessageTagHelper
{
    public FormGroupErrorTagHelper(IHtmlGenerator generator)
        : base(generator) { }

    [HtmlAttributeName("asp-bind")]
    public ModelExpression AspBind
    {
        get => base.For;
        set => base.For = value.Model as ModelExpression;
    }

    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 
        => base.ProcessAsync(context, output);
}

Then I have created a partial view that takes as model a ModelExpression.

@model ModelExpression

<div class="form-group">
    <label asp-bind="@this.Model" class="col-form-label-sm mb-0 pb-0"></label>
    <input asp-bind="@this.Model" class="form-control" />
    <span asp-bind="@this.Model" class="small text-danger"></span>
</div>

and in the end used like bellow

<form asp-controller="Home" asp-action="Index" method="post">
    <fieldset class="card card-body m-0 p-3">
        <legend class="w-auto px-1 py-0 m-0 small font-weight-bold ">With Tag Helpers</legend>
        <partial name="TestGroup" model="this.ModelExpressionProvider.CreateModelExpression(this.ViewData, x => x.Name)" />
        <partial name="TestGroup" model="this.ModelExpressionProvider.CreateModelExpression(this.ViewData, x => x.Age)" />
    </fieldset>
    <div class="d-flex mt-3">
        <input type="submit" class="btn btn-primary px-5" value="Sublit" />
    </div>
</form>

It looks like the created ModelExpression is wrapped in another ModelExpression, where the Model is the actual ModelExpression. I turned it into a ModelExpression and gave it to the For property into already extended TagHelpers.

If you consider go this way you do not need your model wrapper any more, instead of label you can decorate your properties with some Attributes like bellow

using System.ComponentModel.DataAnnotations;

public class ExampleModel
{
    [Required]
    [Display(Name = "Fullname")]
    public string Name { get; set; }

    [Range(18, 100)]
    [Display(Name = "Age")]
    public int Age { get; set; }

    public ExampleModel InnerModel { get; set; }
}
Sign up to request clarification or add additional context in comments.

14 Comments

Thanks for this solution. I have 2 questions: 1) Why we need to extend the taghelpers? I mean how will InputTagHelper know to get asp-for from asp-bind instead? 2) How can I print the [Display(Name = "Fullname")] inside the <label>?
1. asp-for wraps the property allways into modelexpression in order to assign other modelexpression you need to do that directly like in the example. and this assignment happends when you says base.For = (my model expression)
2. there is no need of additional printing asp-for are already bind to the model attribute, that printing what you mean happends behaind, they doing that already for you along with the values and error messages
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.