Razor's Conditional Attributes Bit Me!


Sunday 17 Feb 2013 at 21:00
Development  |  asp.net razor

When ASP.NET MVC 4 was released, Microsoft upgraded the Razor view engine that ships with ASP.NET MVC to version 2 and with it came a number of improvements. One of these improvements was a feature called “conditional attributes”.

Conditional Attributes are a new feature that allows you to shortcut “boilerplate” null check code when rendering an attribute to a HTML element. If you have a model property or a local variable that is used to output the “value” of a HTML element’s attribute that evaluates to NULL, the Razor engine will now automatically discard rendering the entire (empty) attribute.

Thus, whereas we’d previously have to do something like this:

<div @{ if(@Model.ClassName != null) { <text>class="@Model.ClassName"</text> } }>Content</div>

to ensure that, if @Model.ClassName was null, we wouldn’t render the entire class attribute, the new Conditional Attributes feature allows us to do this:

<div class="@Model.ClassName">Content</div>

and the Razor parser is smart enough to not render the class=”” literal attribute text if @Model.ClassName evaluates to null.  So we don’t get this:

<div class="">Content</div>

But instead we get much cleaner markup like this:

<div>Content</div>

Razor’s conditional attributes also work similarly with boolean values, so you can for example, cleverly output checked attributes on an input element defined as a checkbox like so:

<input type="checkbox" checked="@IsChecked">

If @IsChecked evaluates to true, the checked attribute is rendered with a value which is the same name as the attribute:

<input type="checkbox" checked="checked">

However, if @IsChecked evaluates to false, the entire attribute is not rendered.  Andrew Nurse, a developer on Microsoft’s Razor team, has a great blog post about this and the other new features in Razor v2.

So, this is all well and good, however, there is a huge gotcha that you need to be aware of surrounding conditional attributes!  I discovered this when upgrading a project originally built in ASP.NET MVC 3 (which had Razor v1 and thus didn’t have the conditional attributes feature) to ASP.NET MVC 4.  This previously working project suddenly developed bugs that weren’t there before.  Upon inspection, it was due to Razor’s new conditional attributes feature that introduced a breaking change in my code.

Basically, I had a ASP.NET MVC strongly-typed View that displayed a grid of data.  As part of the model for this view was an object used to hold some basic data relating to how the user had configured the grid.  This was simply non-sensitive data such as the number of records per page, the column name upon which the grid was sorted and the sort order (ascending or descending).  This was output to the View as a number of hidden input fields within the view’s form, such that they could be posted back to the server upon each page request:

<input type="hidden" name="pagesize" value="@Model.PagingInfo.ItemsPerPage" />
<input type="hidden" name="sortname" value="@Model.PagingInfo.SortName" />
<input type="hidden" name="sortasc" value="@Model.PagingInfo.SortAscending" />

The problem was within that last line.  The @Model.PagingInfo.SortAscending is a boolean that evaluates to true if the user wants to sort ascending, or false if the user wants to sort descending.  In ASP.NET MVC 3, this would work just fine with the resulting output looking something like:

<input type="hidden" name="sortasc" value="false" />

when the user had elected to sort descending.  However, upon upgrading the project to ASP.NET MVC 4, Razor v2’s conditional attributes feature saw that the @Model.PagingInfo.SortAscending model property was a boolean and that it evaluated to false, and decided not to render the value attribute at all, thus my output became:

<input type="hidden" name="sortasc" />

When the user had selected to sort in an ascending manner, and the value of the @Model.PagingInfo.SortAscending property evaluated to true, the output was even more strange:

<input type="hidden" name="sortasc" value="value" />

This was the “cleverness” of the Razor parser kicking in and outputting the attribute’s name as it’s value when my boolean property evaluated to true.  This makes lots of sense when we’re outputting a series of checkboxes and we want one of them to be checked, which requires the checked=”checked” attribute to be added to the checked element, but not so much sense when we actually want to output the string “true” or “false” as a value attribute’s value in a hidden text input form field!

So whilst the output each time was valid HTML markup with no errors being displayed, this clearly affected the functionality of my page.  POSTs of this page, which would cause the page to redisplay (for example, when the user selected a different sort column or a different sort order) would always result in the grid sorting in a descending manner, irrespective of the user’s choice.

This was due to the Razor model binder finding no suitable value with which to bind the sortasc parameter of the controller method that was invoked when the page was posted back to the server:

public ViewResult List(string searchterm, string sortname, bool sortasc = false, int id = Page, int pagesize = PageSize)

Of course, the sortasc parameter’s default value was then always used, resulting in the “grid is always descending” behaviour!

This was an interested bug to hunt down within my code, and was also a particularly annoying one too, as the code had worked perfectly in ASP.NET MVC 3.  However, once it was discovered how and why this bug reared it’s head, it was also simple enough to fix.

The fix for this is to simply append .ToString() to all boolean variables/model properties that are used purely to render a true or a false attribute’s value on a HTML element.

Thus, my above code was fixed quite simply like so:

<input type="hidden" name="pagesize" value="@Model.PagingInfo.ItemsPerPage" />
<input type="hidden" name="sortname" value="@Model.PagingInfo.SortName" />
<input type="hidden" name="sortasc" value="@Model.PagingInfo.SortAscending.ToString()" />

The addition of the .ToString() forces the evaluation of the boolean and it’s resulting conversion to a string prior to the Razor engine’s parser being able to work it’s “conditional attribute” cleverness.  It simply results in the boolean’s string value being output as the attributes value every time, like this:

<input type="hidden" name="sortasc" value="True" />

So, whilst this issue didn’t manifest itself for me until I upgraded an older ASP.NET MVC 3 project to ASP.NET MVC 4, it’s quite feasible that a developer could write code like this from scratch in MVC 4 and expect the Razor parser to simply output the value of the boolean as the attribute value.  There’s an open case for this in the ASP.NET Web Stack issue tracker on CodePlex and whilst there is a simple enough workaround for the problem, it’s the “breaking change” nature of the issue that is most concerning.

Let’s be careful out there and remember to .ToString() our booleans!