How can we get Asp.Net MVC to display boolean values better?
Lets have a simple model:
public class Options { [Display(Name = "Flux")] public bool EnableFlux { get; set; } [Display(Name = "Advanced Options")] public bool ShowAdvancedOptions { get; set; } [Display(Name = "Do you eat well?")] public bool UserEatsWell { get; set; } }
The default display template isn’t great – it just displays disabled checkboxes:
<h4>Display:</h4> @Html.LabelFor(m => m.ShowAdvancedOptions) @Html.DisplayFor(m => m.ShowAdvancedOptions) @Html.LabelFor(m => m.EnableFlux) @Html.DisplayFor(m => m.EnableFlux) @Html.LabelFor(m => m.UserEatsWell) @Html.DisplayFor(m => m.UserEatsWell)
The result isn’t too pretty:
What I’d like is to have a representative text instead of that poor checkbox. We want different text for different boolean fields: Enable/Disable, Yes/No, Show/Hide, etc.
Luckily, this is quite simple with MVC.
First, since we are using data annotation, that gives us a good place to start. We’ll write an attribute that specifies for each field its display values:
/// <summary> /// For a boolean field, set the display text for <c>true</c> and /// <c>false</c> values. /// </summary> [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public class BooleanDisplayValuesAttribute : Attribute, IMetadataAware { public const string TrueTitleAdditionalValueName = "BooleanTrueValueTitle"; public const string FalseTitleAdditionalValueName = "BooleanFalseValueTitle"; private readonly string _trueValueTitle; private readonly string _falseValueTitle; public BooleanDisplayValuesAttribute(string trueValueTitle, string falseValueTitle) { _trueValueTitle = trueValueTitle; _falseValueTitle = falseValueTitle; } public void OnMetadataCreated(ModelMetadata metadata) { metadata.AdditionalValues[TrueTitleAdditionalValueName] = _trueValueTitle; metadata.AdditionalValues[FalseTitleAdditionalValueName] = _falseValueTitle; } }
We’ve create an attribute that implements the IMetadataAware
interface, so it can add values to ModelMetadata.AdditionalValues
.
We’ll also throw in three useful derived classes:
/// <summary> /// For a boolean field, set the display text for <c>true</c> and /// <c>false</c> values to "Show" and "Hide". /// </summary> public class BooleanDisplayValuesAsShowHideAttribute : BooleanDisplayValuesAttribute { public BooleanDisplayValuesAsShowHideAttribute() : base("Show", "Hide") { } } /// <summary> /// For a boolean field, set the display text for <c>true</c> and /// <c>false</c> values to "Enable" and "Disable". /// </summary> public class BooleanDisplayValuesAsEnableDisableAttribute : BooleanDisplayValuesAttribute { public BooleanDisplayValuesAsEnableDisableAttribute() : base("Enable", "Disable") { } } /// <summary> /// For a boolean field, set the display text for <c>true</c> and /// <c>false</c> values to "Yes" and "No". /// </summary> public class BooleanDisplayValuesAsYesNoAttribute : BooleanDisplayValuesAttribute { public BooleanDisplayValuesAsYesNoAttribute() : base("Yes", "No") { } }
Now we can decorate our model:
public class Options { [Display(Name = "Flux")] [BooleanDisplayValuesAsEnableDisable] public bool EnableFlux { get; set; } [Display(Name = "Advanced Options")] [BooleanDisplayValuesAsShowHide] public bool ShowAdvancedOptions { get; set; } [Display(Name = "Do you eat well?")] [BooleanDisplayValuesAsYesNo] public bool UserEatsWell { get; set; } }
And, the only thing left is to read the values. We’ll write a small HTML Helper:
/// <summary> /// Return options represnting the True and False titles of a /// boolean field. /// </summary> /// <returns>A list with the false title at position 0, /// and true title at position 1.</returns> public static IList<SelectListItem> OptionsForBoolean<TModel, TProperty>( this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) { var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); object trueTitle; metaData.AdditionalValues.TryGetValue( BooleanDisplayValuesAttribute.TrueTitleAdditionalValueName, out trueTitle); trueTitle = trueTitle ?? "Yes"; object falseTitle; metaData.AdditionalValues.TryGetValue( BooleanDisplayValuesAttribute.FalseTitleAdditionalValueName, out falseTitle); falseTitle = falseTitle ?? "No"; var options = new[] { new SelectListItem {Text = (string) falseTitle, Value = Boolean.FalseString}, new SelectListItem {Text = (string) trueTitle, Value = Boolean.TrueString}, }; return options; }
Usage
For displaying values, I added a Display Template for all booleans, but you might prefer a UiHint.
At ~/Views/Shared/DisplayTemplates/Boolean.cshtml
:
@model Boolean @{ var option = Html.OptionsForBoolean(m => m) .Single(o => o.Value == (Model ? Boolean.TrueString : Boolean.FalseString)); } <span data-boolean-value="@Model.ToString().ToLowerInvariant()">@option.Text</span>
Note that I threw in a data attribute, data-boolean-value
, with the boolean value formatted for JavaScript. A better alternative may be the <data>
element, but it is poorly supported at this time.
Result
The same code as above gives:
We can also change the edit mode. For example, here I use a drop-down:
@Html.LabelFor(m => m.ShowAdvancedOptions) @Html.DropDownListFor(m => m.ShowAdvancedOptions, Html.OptionsForBoolean(m => m.ShowAdvancedOptions))
With a little extra code, I also wrote nicer edit templates, which show radio buttons, designed as a switch.
Thanks a lot for taking the time to write this! Solved a bunch of my checkbox + mvc (hint:hidden input fields) + Formmethod.GET problems!
well-done. Very powerful post.
Great work, thanks!
How this can work on Asp net core 2.1? IMetadataAware Interface is not supported from MVC 6 framework onward?
Hello Robert!
I knew this question would come one day…
I don’t know. I looked around and didn’t find a direct replacement for this technique, but here are a few pointers I’ve found:
GitHub – Restore missing `ModelMetadata` properties (additional metadata)
AdditionalMetadataAttribute Anyone?
Microsoft Docs – IDisplayMetadataProvider Interface
I don’t have enough time at the moment to try and implement it, and there may be an entirely new/better approach in Core that I just don’t know, but please let me know if you find anything that works.
Thanks!
Kobi
Thanks for quick reply. I still haven’t found anything useful and I am not near the level of coding you are and I am not able to replicate all those metadata templates yet.
Currently I am using:
1) Edit Mode:
As a workaround I am just using this code on the edit pages:
@Html.DropDownListFor(m => m.IsActive, new SelectList(
new[] {
new { Value = “”, Text = “– Choose YES or NO –” },
new { Value = “true”, Text = “YES” },
new { Value = “false”, Text = “NO” },
},
“Value”,
“Text”
))
Yes, No, Null, text can easily by replaced by other text, but this is not neat and repeatable as your code. I read there is an option somewhere to create DropDownList template, but I did not get to that knowledge yet.
2) List/Index Mode:
Here I am using Html.Helpers
using System;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace PMS.Infrastructure
{
public static class HTMLHelpers
{
public static IHtmlContent YesNoNothing(this IHtmlHelper htmlHelper, bool? yesNo)
{
string s = “”;
if (yesNo == null) { s = “-“; };
if (yesNo == true) { s = “Yes”; };
if (yesNo == false) { s = “No”; };
return new HtmlString(s);
}
}}
3) This helper file is registered in _ViewImports.cshtml in Shared folder:
Enables the Xbuilt-in tag helpers, which I can use to create HTML elements that reflect the configuation of the application
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Enables html tag helper
@using PMS.Infrastructure
4) and in Index.cshtml page I am using this code:
@Html.YesNoNothing(item.IsActive)
That is the only current workaround I have found, using brute force and basic coding, but it works.
I wish I could improve it and make it repeatable as your code was in MVC 5.
It makes me very sad to find this only to discover it doesn’t work with core 2.x 😦