MVC 3: Conditionally Adding the Disabled Attribute with the HtmlHelpers
Asked Answered
C

9

79

I have an ASP.Net MVC 3 web application and I am adding a check box to a view page using the HtmlHelper class, like this...

@Html.CheckBox("CheckBox1", true, new { @class = "Class1" })

What I want to do is conditionally add the disabled attribute based on a view state property. Basically the following would be ideal...

@Html.CheckBox("CheckBox1", true, new { @class = "Class1", @disabled = Model.ReadOnly })

Unfortunately, due to the nature of the disabled attribute, this will not work because any value assigned to the disabled attribute (even "false") will be translated as true.

I have already thought of a few solutions to get round this problem, so the question is not how can I do this. But rather, is there a simple way like the desired method above? or do I have to resort to one of the following?..

What I know I could do...

  1. Create an if/else statement and write to different Html.CheckBox lines (not great for readability - and possible with throw a mark-up warning - not sure)

  2. Skip the HtmlHelper class and hand write the tag allowing for better conditionally attributes (keeps the code shorter, but adds inconsistency)

  3. Create a custom helper that takes a "disabled" parameter (the cleanest solution, but requires undesired extra methods - probably the best option so far though)

Coke answered 2/11, 2011 at 9:33 Comment(2)
Have a look at my answer here (uses the 3rd approach you listed): https://mcmap.net/q/212987/-mvc3-conditionally-disable-html-textboxforGrillroom
Possible duplicate of Set disable attribute based on a condition for Html.TextBoxForWhippet
A
57

Define this somewhere in your view/helpers

@functions {
 object getHtmlAttributes (bool ReadOnly, string CssClass) 
 {
     if (ReadOnly) {
         return new { @class = CssClass, @readonly = "readonly" };
     }
     return new { @class = CssClass };
 }
}

Then use :

@Html.TextBox("name", "value", @getHtmlAttributes(Model.ReadOnly, "test"))
Abramson answered 2/11, 2011 at 15:14 Comment(5)
Good answer. I had ended up doing the same logic except with a single line (ternary, I think) condition as part of the Html.CheckBox call. Your solution is much nicer, thanks ;)Coke
Thanks, for information, I suspect that the better way is to define an EditorTemplate for the model, defining the function internally and providing also some sort of client side jQuery magic for doing everything. For example on the Load event of a containing DIV.Abramson
mmm.. the above example doesn't work when I use it with .TextBoxFor() (Instead of .TextBox() ) - Anonymous type projection initializer should be simple name or member access expression...Monecious
How you do the same for add disable optionally ?Nubble
Just add a bool parameter to the function and set the value of @readonly accordingly. I've lost contact with .NET MVC so don't really know if this answer still applies with latest version (I hope they've added some better way to do this).Abramson
L
34

Here's my answer from this similar question: https://mcmap.net/q/212989/-conditionally-disable-html-dropdownlist


I created the following Helper - it takes a boolean and an anonymous object. If disabled is true, it adds the disabled attribute to the anonymous object (which is actually a Dictionary) with the value "disabled", otherwise it doesn't add the property at all.

public static RouteValueDictionary ConditionalDisable(
   bool disabled, 
   object htmlAttributes = null)
{
   var dictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);

   if (disabled)
      dictionary.Add("disabled", "disabled");

   return dictionary;
}

An example of it in action:

@Html.TextBoxFor(m => m.SomeProperty,    
   HtmlHelpers.ConditionalDisable(true, new { @class = "someClass"))

One huge advantage to this approach for me was that it works with virtually all of the MVC HtmlHelpers since they all have Overloads that accept a RouteValueDictionary instead of an anonymous object.

Caveats:
HtmlHelper.AnonymousObjectToHtmlAttributes() uses some fancy code ninja work to get things done. I'm not entirely sure how performant it is... but it's been sufficient for what I use it for. Your mileage may vary.

I don't especially like the name of it - but I couldn't come up with anything better. Renaming is easy.

I also don't love the usage syntax - but again I couldn't come up with anything better. It shouldn't be difficult to change. An extension method on object is one idea... you'd end up with new { @class = "someClass" }.ConditionalDisable(true) but then if you only want the disable attribute and don't have anything additional to add you end up with something gross like new {}.ConditionalDisable(true); and you also end up with an extension method that shows up for all object... which is probably not desirable.

Lianaliane answered 17/12, 2012 at 22:5 Comment(3)
A most elegant solution. This is the best I have found. I considered an extension method before but it would just be far too general.Tentative
This is a great solution, but I can't seem to get it to work with a Html.ActionLinkTagliatelle
Good solution but it does not work with Html.EditorFor as wellTheomachy
V
14

If you want more terse syntax without requiring a helper function, you could use a ternary statement when defining the dictionary used for the html attributes of the @HTML.Checkbox helper...

@Html.CheckBox("CheckBox1", true, Model.ReadOnly 
       ? new { @class = "Class1", @disabled = Model.ReadOnly } 
       : null)

In this case is Model.ReadOnly is false, null gets passed as the dictionary of html attributes.

Vanillic answered 25/6, 2013 at 16:11 Comment(4)
This could get quite verbose if you are setting other attributes.Sciatica
@Sciatica couldn't you then just setup a property on your view[model] that does all the heavy calculations for you? Then in the cshtml, it's as simple as the example above.Deese
This doesn't work if you still want to set the @class property in the false condition.Frizz
This does not work. I just tried it with @Html.DropDownListFor(model => model.FileType, (SelectList)ViewData["FileTypes"], "...", new { @class = "custom-select chosen-select col-md-2", @disabled = canEdit ? null : "disabled"}), and I get a disabled attribute without value like <select class="..." disabled id="FileType"><option value="">1st option</option>...</select>Externalization
R
5

What do you think about my simple solution? It works easily with both possible HtmlAttributes types:

  • Dictionary<string, object>
  • Anonymous Object:

First add the following simple extension class to your project:

public static class HtmlAttributesExtensions
{
    public static IDictionary<string, object> AddHtmlAttrItem(this object obj, string name, object value, bool condition)
    {
        var items= !condition ? new RouteValueDictionary(obj) : new RouteValueDictionary(obj) {{name, value}};
        return UnderlineToDashInDictionaryKeys(items);
    }
    public static IDictionary<string, object> AddHtmlAttrItem(this IDictionary<string, object> dictSource, string name, object value, bool condition)
    {
        if (!condition)
            return dictSource;

        dictSource.Add(name, value);
        return UnderlineToDashInDictionaryKeys(dictSource);
    }
    private static IDictionary<string, object> UnderlineToDashInDictionaryKeys(IDictionary<string,object> items)
    {
        var newItems = new RouteValueDictionary();
        foreach (var item in items)
        {
            newItems.Add(item.Key.Replace("_", "-"), item.Value);
        }
        return newItems;
    }
}

Now in View:

Example1 (HtmlAttributes type as Anonymous Object)

@{
  var hasDisabled=true; 
}

@Html.CheckBox("CheckBox1"
              , true
              , new { @class = "Class1"}
               .AddHtmlAttrItem("disabled", "disabled", hasDisabled))
.

Example 2 (HtmlAttributes type as Dictionary<string, object>)

@Html.CheckBox("CheckBox1"
              , true
              , new Dictionary<string, object> { { "class", "Class1" }
               .AddHtmlAttrItem("disabled", "disabled", hasDisabled))
.

Now just change the hasDisabled value to true or false!


Example3 (Multiple conditional properties)

@{
  var hasDisabled=true;
  var hasMax=false ;
  var hasMin=true ;
}

@Html.CheckBox("CheckBox1"
              , true
              , new { @class = "Class1"}
               .AddHtmlAttrItem("disabled", "disabled", hasDisabled)
               .AddHtmlAttrItem("data-max", "100", hasMax)
               .AddHtmlAttrItem("data-min", "50", hasMin))
.
Ravenravening answered 8/9, 2016 at 16:56 Comment(1)
This is great, thank you! One comment though: I recommend using HtmlHelper.AnonymousObjectToHtmlAttributes instead of UnderlineToDashInDictionaryKeys because it's already built-in. ;)Koehn
H
3

Performing the addition of the disabled attribute client side works for me. Note you should check which fields are allowed to be edited server side, but that is true for where the disabled attribute is declared decoratively also.

In this example I have disabled all childeren of a form using jQuery.

    if (Model.CanEdit)
    {
        <script type="text/javascript">

            $(document).ready(function() {

                $('#editForm *').attr('disabled', true);
            });

        </script>
    }
Hernardo answered 5/7, 2013 at 14:13 Comment(0)
G
1
@Html.TextBoxFor(m => m.FieldName, Html.FixBoolAttributes(new {
    @class = "myClass",
    @readonly = myFlag  
}))


public static class BooleanAttributeFix
{
    /// <summary>
    /// Normalises HTML boolean attributes so that readonly=true becomes readonly="readonly" and
    /// readonly=false removes the attribute completely.
    /// </summary>
    /// <param name="htmlHelper"></param>
    /// <param name="htmlAttributes"></param>
    /// <returns></returns>
    public static RouteValueDictionary FixBoolAttributes(this HtmlHelper htmlHelper, object htmlAttributes)
    {
        var attrs = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);

        foreach(var attrName in new[] { "disabled", "readonly" })
        {
            object value;
            if(attrs.TryGetValue(attrName, out value))
            {
                if(isTruthy(value))
                {
                    // Change from readonly="true" to readonly="readonly"
                    attrs[attrName] = attrName; 
                }
                else
                {
                    // Remove attribute entirely
                    attrs.Remove(attrName); 
                }
            }
        }
        return attrs;
    }

    /// <summary>
    /// Apply similar loose rules like javascript does for whether a value is true or not.
    /// e.g. 1 = true, non-empty string = true and so on.
    /// </summary>
    /// <param name="val"></param>
    /// <returns></returns>
    private static bool isTruthy(object val)
    {   
        if(val == null)
            return false;

        if(val is string)
        {
            return !String.IsNullOrEmpty((string)val);
        }

        Type t = val.GetType();

        if(t.IsValueType && Nullable.GetUnderlyingType(t) == null)
        {
            // If a non-nullable value type such as int we want to check for the
            // default value e.g. 0.
            object defaultValue = Activator.CreateInstance(t);

            // Use .Equals to compare the values rather than == as we want to compare
            // the values rather than the boxing objects.
            // See https://mcmap.net/q/212990/-comparing-boxed-value-types
            return !val.Equals(defaultValue);
        }

        return true;
    }
}
Gluconeogenesis answered 9/9, 2015 at 14:43 Comment(0)
H
0

I liked @gb2d answer, here it is in JS using getElementsByClassName taken from here getElementByClass().setAttribute doesn't work

<script type="text/javascript">
    var list, index;
    list = document.getElementsByClassName("form-control");
    for (index = 0; index < list.length; ++index) {
        list[index].setAttribute('disabled', true);
    }
</script>
Herries answered 10/12, 2019 at 23:55 Comment(0)
A
0

I think the top two answers are valid and solve the problem, but a different style which more successfully retains the original/traditional format (lowering the learning curve in team environments);

@Html.TextBoxFor(m => m.FirstName, new { @class = "form-control", disabled = Model.IsReadOnly }.RemoveFalseyAttributes())

The helper method is a static class which is included in the _ViewImports.cshtml and in this vien will only appear in your intellisense when working on views.

The method itself removes the disabled attribute if it is false and can be extended to check other attribute easily.

public static class ViewUtility
{
    /// <summary>
    /// Removes attributes which when set to false should not be included on the HTML attribute
    /// </summary>
    /// <param name="htmlAttributes"></param>
    /// <returns></returns>
    public static IDictionary<string, object> RemoveFalseyAttributes(this object htmlAttributes)
    {
        var routeValueDictionary = new RouteValueDictionary(htmlAttributes);
        var attributeDictionary = new Dictionary<string, object>();

        foreach (var kvp in routeValueDictionary)
        {
            if (kvp.Key == "disabled" && kvp.Value.Equals(false))
            {
                continue;
            }
            attributeDictionary.Add(kvp.Key, kvp.Value);
        }

        return attributeDictionary;
    }
}
Autohypnosis answered 3/10, 2023 at 9:46 Comment(0)
R
0

This overload of Html.CheckBox() accpets an IDictionary<string, object>, so conditionally adding the disabled attribute is achieved with:

 @{
     IDictionary<string, object> attributes = new Dictionary<string, object>();
     if (Model.ReadOnly)
     {
         attributes.Add("disabled", "true");
     }
 }

@Html.CheckBox("nameAttribute", Model.IsChecked, attributes)

Ryder answered 12/1 at 16:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.