How to customize the EditorFor CSS with razor

2019-02-25 05:48发布

问题:

I have this class

public class Contact
{
    public int Id { get; set; }
    public string ContaSurname { get; set; }
    public string ContaFirstname { get; set; }
    // and other properties...
}

And I want to create a form that allo me to edit all those fields. So I used this code

<h2>Contact Record</h2>

@Html.EditorFor(c => Model.Contact)

This works fine, but I want to customize how the elements are displayed. For instance I want each field to be displayed in the same line as its label. Because now, the generated html is like this :

<div class="editor-label">
  <label for="Contact_ContaId">ContaId</label>
</div>
<div class="editor-field">
  <input id="Contact_ContaId" class="text-box single-line" type="text" value="108" name="Contact.ContaId">
</div>

回答1:

Create a partial view called Contact.cshtml with your custom markup in Views/Shared/EditorTemplates. This will override the default editor.

As noted by @smartcavemen, see Brad Wilson's blog for an introduction to templates.



回答2:

I agree to the solution of jrummell above: When you use the EditorFor-Extension, you have to write a custom editor template to describe the visual components.

In some cases, I think it is a bit stiff to use an editor template for several model properties with the same datatype. In my case, I want to use decimal currency values in my model which should be displayed as a formatted string. I want to style these properties using corresponding CSS classes in my views.

I have seen other implementations, where the HTML-Parameters have been appended to the properties using annotations in the Model. This is bad in my opinion, because view information, like CSS definitions should be set in the view and not in a data model.

Therefore I'm working on another solution:

My model contains a decimal? property, which I want to use as a currency field. The Problem is, that I want to use the datatype decimal? in the model, but display the decimal value in the view as formatted string using a format mask (e.g. "42,13 €").

Here is my model definition:

[DataType(DataType.Currency), DisplayFormat(DataFormatString = "{0:C2}", ApplyFormatInEditMode = true)]
public decimal? Price { get; set; }

Format mask 0:C2 formats the decimal with 2 decimal places. The ApplyFormatInEditMode is important, if you want to use this property to fill a editable textfield in the view. So I set it to true, because in my case I want to put it into a textfield.

Normally you have to use the EditorFor-Extension in the view like this:

<%: Html.EditorFor(x => x.Price) %>

The Problem:

I cannot append CSS classes here, as I can do it using Html.TextBoxFor for example.

To provide own CSS classes (or other HTML attributes, like tabindex, or readonly) with the EditorFor-Extension is to write an custom HTML-Helper, like Html.CurrencyEditorFor. Here is the implementation:

public static MvcHtmlString CurrencyEditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, Object htmlAttributes)
{
  TagBuilder tb = new TagBuilder("input");

  // We invoke the original EditorFor-Helper
  MvcHtmlString baseHtml = EditorExtensions.EditorFor<TModel, TValue>(html, expression);

  // Parse the HTML base string, to refurbish the CSS classes
  string basestring = baseHtml.ToHtmlString();

  HtmlDocument document = new HtmlDocument();
  document.LoadHtml(basestring);
  HtmlAttributeCollection originalAttributes = document.DocumentNode.FirstChild.Attributes;

  foreach(HtmlAttribute attr in originalAttributes) {
    if(attr.Name != "class") {
      tb.MergeAttribute(attr.Name, attr.Value);
    }
  }

  // Add the HTML attributes and CSS class from the View
  IDictionary<string, object> additionalAttributes = (IDictionary<string, object>) HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);

  foreach(KeyValuePair<string, object> attribute in additionalAttributes) {
    if(attribute.Key == "class") {
      tb.AddCssClass(attribute.Value.ToString());
    } else {
      tb.MergeAttribute(attribute.Key, attribute.Value.ToString());
    }
  }

  return MvcHtmlString.Create(HttpUtility.HtmlDecode(tb.ToString(TagRenderMode.SelfClosing)));
}

The idea is to use the original EditorFor-Extension to produce the HTML-Code and to parse this HTML output string to replace the created CSS Html-Attribute with our own CSS classes and append other additional HTML attributes. For the HTML parsing, I use the HtmlAgilityPack (use google).

In the View you can use this helper like this (don't forget to put the corresponding namespace into the web.config in your view-directory!):

<%: Html.CurrencyEditorFor(x => x.Price, new { @class = "mypricestyles", @readonly = "readonly", @tabindex = "-1" }) %>

Using this helper, your currency value should be displayed well in the view.

If you want to post your view (form), then normally all model properties will be sent to your controller's action method. In our case a string formatted decimal value will be submitted, which will be processed by the ASP.NET MVC internal model binding class.

Because this model binder expects a decimal?-value, but gets a string formatted value, an exception will be thrown. So we have to convert the formatted string back to it's decimal? - representation. Therefore an own ModelBinder-Implementation is necessary, which converts currency decimal values back to default decimal values ("42,13 €" => "42.13").

Here is an implementation of such a model binder:

public class DecimalModelBinder : IModelBinder
{

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      object o = null;
      decimal value;

      var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
      var modelState = new ModelState { Value = valueResult };

      try {

        if(bindingContext.ModelMetadata.DataTypeName == DataType.Currency.ToString()) {
          if(decimal.TryParse(valueResult.AttemptedValue, NumberStyles.Currency, null, out value)) {
            o = value;
          }
        } else {
          o = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
        }

      } catch(FormatException e) {
        modelState.Errors.Add(e);
      }

      bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
      return o;
    }
}

The binder has to be registered in the global.asax file of your application:

protected void Application_Start()
{
    ...

    ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
    ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());

    ...
}

Maybe the solution will help someone.