3

I am building a project with a lot of common code regarding the razor view.

Example:

<div class="form-group">
    @Html.LabelFor(model => model.LayoutFrontAmount, htmlAttributes: new { @class = "control-label col-xs-12 col-sm-4 col-md-3" })
    <div class="col-xs-4 col-sm-2 col-md-2 col-lg-1">
        @Html.EditorFor(model => model.LayoutFrontAmount, new { htmlAttributes = new { @class = "form-control" } })
    </div>
    <div class="col-xs-12 col-md-8 col-sm-offset-4 col-md-offset-3">
        <span class="help-block">@Html.ValidationMessageFor(model => model.LayoutFrontAmount, "", new { @class = "text-danger" })</span>
    </div>
</div>

<div class="form-group">
    @Html.LabelFor(model => model.LayoutFrontBackAmount, htmlAttributes: new { @class = "control-label col-xs-12 col-sm-4 col-md-3" })
    <div class="col-xs-4 col-sm-2 col-md-2 col-lg-1">
        @Html.EditorFor(model => model.LayoutFrontBackAmount, new { htmlAttributes = new { @class = "form-control" } })
    </div>
    <div class="col-xs-12 col-md-8 col-sm-offset-4 col-md-offset-3">
        <span class="help-block">@Html.ValidationMessageFor(model => model.LayoutFrontBackAmount, "", new { @class = "text-danger" })</span>
    </div>
</div>

<div class="form-group">
    @Html.LabelFor(model => model.LayoutTRC, htmlAttributes: new { @class = "control-label col-xs-12 col-sm-4 col-md-3" })
    <div class="col-xs-4 col-sm-2 col-md-2 col-lg-1">
        @Html.EditorFor(model => model.LayoutTRC, new { htmlAttributes = new { @class = "form-control" } })
    </div>
    <div class="col-xs-12 col-md-8 col-sm-offset-4 col-md-offset-3">
        <span class="help-block">@Html.ValidationMessageFor(model => model.LayoutTRC, "", new { @class = "text-danger" })</span>
    </div>
</div>

The only thing that changes is the Model's property.

Is there a way to replace all the code with something like:

@TemplateName1(model => model.LayoutFrontAmount)
@TemplateName1(model => model.LayoutFrontBackAmount)
@TemplateName1(model => model.LayoutTRC)
Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
Patrick
  • 2,995
  • 14
  • 64
  • 125
  • You can create a HtmlHelper extension method that generates label, form control and validation message (and the associated `div` elements) which you could use as (say) `@Html.MyEditorFor(model => model.LayoutFrontAmount)` –  Jan 21 '16 at 00:07
  • The [3rd option in this answer](http://stackoverflow.com/questions/26162218/editortemplate-for-dropdownlist/26417466#26417466) gives an example –  Jan 21 '16 at 00:10
  • Thanks @StephenMuecke but I would prefer a closest and cleaner HTML structure, it seams dificult to look and understand where is what – Patrick Jan 21 '16 at 00:12
  • Sorry, not sure what you mean. Once you create the extension method, all you need in your view is 3 line of code (`@Html.MyEditorFor(m => m.someProperty)`) to generate all the html exactly as you have shown in your question –  Jan 21 '16 at 00:17
  • Like @Chase's answer, I can look to the View and see the HTML structure – Patrick Jan 21 '16 at 00:21
  • That's extremely limited - its only works for `string` - you would need the create another one for every type such as `DateTime`, `int` etc. And in any case your already using extension methods (`LabelFor()`, `EditorFor()` etc) which don't show you the html structure –  Jan 21 '16 at 00:24
  • If it would be to just share markup/model usage helpers would help (http://weblogs.asp.net/scottgu/asp-net-mvc-3-and-the-helper-syntax-within-razor) but I don't believe it can work with generic version of `Html.EditorFor` – Alexei Levenkov Jan 21 '16 at 00:25
  • Not to mention it will never work for nested properties because it will not add the correctly prefixed `name` attribute so model binding will fail when you submit the form –  Jan 21 '16 at 00:26
  • @StephenMuecke I am affraid that building all the code in my example based on the option 3 that you refer can be a nightmare :S – Patrick Jan 21 '16 at 00:32
  • Its only a 16 lines of code! and then when ever you want to create the html for each property (i.e. your 3 `
    ` elements, the `
    –  Jan 21 '16 at 00:39
  • I kind of agree with @Chase opinion regarding maintance if I want to change details in the Helper, I would need to compile it every time, but maybe your sugestion is faster regarding performance then using Views, no? – Patrick Jan 21 '16 at 00:41
  • What do you mean change the details in the helper? Your question specifically identified that you want to generate the same html and _only thing that changes is the Model's property_ Both are so fast its not an issue, but the extension method means you have one piece of reusable code (DRY) whereas using partials mean you need one for each property type and it will not work for nested properties - a nightmare to maintain –  Jan 21 '16 at 00:55
  • Would it be to much from me if I ask you to answer the question with an example of my code for a property based on your built-in helper suggestion? – Patrick Jan 21 '16 at 00:59
  • Sure - but give me 30 min or so –  Jan 21 '16 at 01:04
  • I've changed title - see if it aligns with your intention (hopefully easier to find in search that way). – Alexei Levenkov Jan 21 '16 at 17:19
  • Well... :) I think mine was more easy to associate with, but that's only my opinion :) – Patrick Jan 21 '16 at 17:21

2 Answers2

6

You can create a HtmlHelper extension method that will generate all the html for a property, including the label and input element

using System;
using System.Linq.Expressions;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;

namespace YourAssembly.Html
{
  public static class BootstrapHelper
  {
    public static MvcHtmlString BootstrapEditorFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression)
    {      
      MvcHtmlString label = LabelExtensions.LabelFor(helper, expression, new { @class = "control-label col-xs-12 col-sm-4 col-md-3" });
      MvcHtmlString editor = EditorExtensions.EditorFor(helper, expression, new { htmlAttributes = new { @class = "form-control" } });
      MvcHtmlString validation = ValidationExtensions.ValidationMessageFor(helper, expression, null, new { @class = "text-danger" });

      // Build the input elements
      TagBuilder editorDiv = new TagBuilder("div");
      editorDiv.AddCssClass("col-xs-4 col-sm-2 col-md-2 col-lg-1");
      editorDiv.InnerHtml = editor.ToString();
      // Build the validation message elements
      TagBuilder validationSpan = new TagBuilder("span");
      validationSpan.AddCssClass("help-block");
      validationSpan.InnerHtml = validation.ToString();
      TagBuilder validationDiv = new TagBuilder("div");
      validationDiv.AddCssClass("col-xs-12 col-md-8 col-sm-offset-4 col-md-offset-3");
      validationDiv.InnerHtml = validationSpan.ToString();
      // Combine all elements
      StringBuilder html = new StringBuilder();
      html.Append(label.ToString());
      html.Append(editorDiv.ToString());
      html.Append(validationDiv.ToString());
      // Build the outer div
      TagBuilder outerDiv = new TagBuilder("div");
      outerDiv.AddCssClass("form-group");
      outerDiv.InnerHtml = html.ToString();
      return MvcHtmlString.Create(outerDiv.ToString());
    }
  }
}

Then you can register this in your web.config file (means you do not need @using ... in the view

<namespaces>
  <add namespace="System.Web.Mvc" />
  ....
  <add namespace="yourAssembly.Html " /> // add this
</namespaces>

Now in your main view your can generate all the html shown in your question with just the following 3 lines of code

@Html.BootstrapEditorFor(m => m.LayoutFrontAmount)
@Html.BootstrapEditorFor(m => m.LayoutFrontBackAmount)
@Html.BootstrapEditorFor(m => m.LayoutTRC)

And if you want this to be reusable across multiple projects, compile it in separate dll and add a reference to it in your projects.

  • Nicely done. Side note: While I believe this is the only approach to achieve what OP is asking (as you can't really go generic in Razor views/helpers) I'd seriously consider to keep duplicated HTML markup over this C# version. – Alexei Levenkov Jan 21 '16 at 08:17
  • @Stephen Thank you very much for the example :) when I woke up today I had a sudden thought, if I need to interacte with the elements via jQuery, I have no chance to do it right? – Patrick Jan 21 '16 at 16:46
  • @AlexeiLevenkov hi, what do you mean by duplicated HTML markup? – Patrick Jan 21 '16 at 16:46
  • @Patrick I mean I'd prefer HTML code you posted in the question - while it has duplication most people will be able to glance at it and understand. Looking at code Stephen provided only tiny portion of developers would be able to reason on what resulting output would be or be able to quickly adjust it. In some cases it is ok (i.e. identical layout must show up in huge number of places), so one need to carefully weight which approach is more sustainable for particular case/ team of developers. – Alexei Levenkov Jan 21 '16 at 17:13
  • Its still creating exactly the same html output as the code in your question (and your inputs have `id` attributes) so you can interact with jQuery in exactly the same way you were before. –  Jan 21 '16 at 22:38
1

Depends on what the types are for each of LayoutFrontAmount, LayoutFrontBackAmount, and LayoutTRC. Are these all strings? If so, you could have a common view file that stores the template for displaying each, and then display each of them by using @Html.Partial() in your primary view:

MyView.cshtml

@model string
<div class="form-group">
    @Html.LabelFor(model => Model, htmlAttributes: new { @class = "control-label col-xs-12 col-sm-4 col-md-3" })
    <div class="col-xs-4 col-sm-2 col-md-2 col-lg-1">
        @Html.EditorFor(model => Model, new { htmlAttributes = new { @class = "form-control" } })
    </div>
    <div class="col-xs-12 col-md-8 col-sm-offset-4 col-md-offset-3">
        <span class="help-block">@Html.ValidationMessageFor(model => Model, "", new { @class = "text-danger" })</span>
    </div>
</div>

And then in your main view file you could render each of LayoutFrontAmount, LayoutFrontBackAmount, and LayoutTRC as follows:

@Html.Partial("MyView", Model.LayoutFrontAmount);
@Html.Partial("MyView", Model.LayoutFrontBackAmount);
@Html.Partial("MyView", Model.LayoutTRC);

If they are different types, that poses a bigger challenge.

Chase
  • 934
  • 6
  • 18
  • Hi, thanks. By challenge you mean that if I have a diferent type, I will need to create a "MyViewInt" and a "MyViewString"? – Patrick Jan 21 '16 at 00:20
  • If they are of different types, it becomes harder to unify them under one model view. If it's just integers and strings you're using, you could easily convert the integer to a string and pass it to that same model. However, the editor for each of those items will be based off of the model type specified at the top of the view. If you convert an int to a string and use a string editor, you would have to parse the integer from the string after editing. – Chase Jan 21 '16 at 00:34
  • Regarding performance compared with @Stephen's answer to use built-in helper methods? – Patrick Jan 21 '16 at 00:36
  • Not sure on performance impact, but doing an HtmlHelper extension method is less flexible and less maintainable, in my opinion. The HtmlHelper extension would have to be re-compiled every time you wanted to make changes to how your view is displayed (which happens frequently in practice). If you do a purely view-based route using only cshtml files, you can edit the files without having to re-compile any source code. – Chase Jan 21 '16 at 00:39