Adding data attributes to each option rendered by MVC DropDownListFor HTML Helper

Quite often you have a scenario whereby a parent dropdown causes another element to be updated based on what is selected, but not on the value selected directly but rather another piece of information related to the option selected.

For example, you want to show a country name in a textbox based on the city a user has selected in a dropdown, but the option value of each city is its primary key, not its associated parent country name so you have no direct link between option selected and a country name.

There are a number of ways to proceed including hitting the server via Ajax on city dropdown change to get the associated country and outputting a client side lookup table of sorts. A simple approach however is just to output the secondary information (in the form of a data-* attribute) to each dropdown option along with its primary value which will give you a select box whose HTML is similar to below:

image004

After this, extracting country name from the selected option is easy.

In MVC how we’d commonly output a select box would be using a DropDownListFor HTML helper control, however if you want to add data attributes to each option when using a DropDownListFor control…well, you can’t but you can ditch the DropDownListFor helper and just output your select box manually using a foreach in razor code. Sample Razor code which will allow you to do this is below, followed by JQuery which helps you to extract the data attribute from the selected dropdown item (source code is available below).

image001

image002

Overview of steps to add data-* annotations

  • Manually output the select tag, with an id/class/name which you can use to hook a change event to using JQuery
  • Output an option for each of your collection items, to include one or more data attributes (in this case data-country) and a test to check if something should be selected.
  • In jQuery, add a change event to your dropdown and extract the value by using the :selected pseudo class  and the data function for getting data-* attribute values.
  • Note that even though you are manually outputting your select, you can still use strongly typed LabelFor, ValidationMessageFor (assuming the names match) etc. controls around your manually outputted dropdown.
  • Note the use of @Html.NameFor when generating the name of the dropdown. You’ll want to use the name which matches the viewmodel property so binding occurs correctly. This helper avoids the need for hard coding.
  • Watch out for the razor syntax to output text from within an if block. You will likely need the text tag or it’s alternative :@

Source Code

WordPress messes up code so I just put the images in above, but you can see the source for a sample manually outputted dropdown on .Net Fiddle

Posting disabled checkboxes from MVC Razor views

In ASP.Net MVC a common technique when dropdowns and textboxes are required to be disabled but their values still need posting back is to include a hidden backing field corresponding to the same model property. In the case of CheckBoxFor the idea is the same, except there is one extra peculiarity with CheckBoxFor due to the fact that for this HMTL  helper MVC renders a hidden text field of its own (along with the checkbox) which does not have the disabled attribute even if you have added it to the main CheckBoxFor HTML Helper. For example here is what is rendered for a disabled checkbox whose underlying bool property is set to true.

hiddentextfield

As nothing gets sent to the server if the checkbox is not checked, the binder has nothing to bind to so MVC always renders this hidden field and always sets its value to false. Due to the fact it appears after the checkbox, the true from the checkbox will bind first if the user has checked it, as MVC always binds to the first matched property. Therefore when we are dealing with disabled but checked checkboxes and we want to post the value, we need to ensure our hidden field appears before the @Html.CheckBoxFor hidden field. The first image below for example always bind as false (even though the checkbox is checked), while the second one binds are true.

after-checkbox

before-checkbox

I think this is a common enough problem, so hopefully the above helps.

Related Links

Credit to Darin Dimitrov for his answer on stackoverflow which outlines the approach above, however crucially he does not mention the importance of having the backing field before the @Html.CheckBoxFor statement which is why I imagine some people mentioned in the comments that a hidden backing field did not work for them.

MVC HTML helpers read from modelState before looking in the model

If you have posted back a form to an MVC action method and want to update some of the bound viewModel properties in that action method before redisplaying the same view, you might run into a problem whereby your HTML helpers are displaying the values as posted at binding time, but not as they were after manipulation.

Just before calling the view statement or indeed in the razor markup itself you can set a breakpoint and verify your viewModel/model have the updated values but still the old values render. This is because MVC assumes that if you’re rendering a view in response to an HTTP POST then you’re likely to be redisplaying a form that has failed validation. Since some data entered by the user and posted back may not be compatible with the model (i.e. posting a decimal number back when the corresponding model property is int) and thus MVC can’t store them in the model it stores all binded values into ModelState and looks in that before the model when rendering a view using HTML helpers such as TextBoxFor.

Example of HTML helper using ModelState not the model to render its value

For example below we can see what was posted (1) and the value of the viewModel/Model when sending it to the view (2). As we are using Html.TextBoxFor however, the rendered data is still showing ‘John’ and ‘Smith’.

viewmodel-after-binding
viewmodel-after-manual-changes

rendered-page-with-binded-values

Clearing ModelState so the model values will be used

The fix for this is to clear either ModelState completely as in 4a or just clear the ModelState properties which are relevant as in 4b images below. After clearing ModelState we can see that the manually updated properties are now displayed.

clearing specific modelstate

rendered-page-with-manual-values

I’m not sure how known this behaviour is or how many problems it causes developers but it’s one to watch out for anyhow.

Related Links

Rick Strahl’s ‘ASP.NET MVC Postbacks and HtmlHelper Controls ignoring Model Changes‘ blog post describes this behaviour and also has a section on the rationale of having MVC work this way. 

Simon Ince’s article ‘ASP.NET MVC’s Html Helpers Render the Wrong Value!’ offers a number of alternative approaches that you could use rather than clearing ModelState such as not using html helpers and implementing the Post-Redirect-Get pattern.

MVC Dropdownlistfor defaulting to previously selected value when using a single SelectListItem List

Watch out for this one. If you have created a single list of SelectListItems and use this viewModel property to populate multiple DropDownListFor HTML helper controls, you will likely notice some strange behaviour.

What happens is that if one DropDownListFor has a selected value (ie. its bound property isn’t null) all subsequent DropDownListFor which are populated using the same SelectListItem List and don’t have a selected value have their default selection changed to whatever the previously selected value was. This happens even though expected behaviour would be to just leave the subsequent DropDownListFor selected to their default values.

As an example, this problem has the potential to occur below. If BirthCountry was selected to say Canada, and ResidenceCountry had no value, the 2nd dropdown would be defaulted to Canada and not “” as explicitly specified.

public IEnumerable<SelectListItem> Countries {get;set;}
@Html.DropDownListFor(x=>x.BirthCountry,Model.Countries,"") @Html.DropDownListFor(x=>x.ResidenceCountry,Model.Countries,"")

This is because SelectListItem is a reference type so when you change the Selected property to true, all dropdowns which use that SelectListItem will reflect this.

Always use distinct SelectListItem lists for each DropDownListFor

You can do this by creating them in the viewModel like:

public IEnumerable<SelectListItem> BirthCountries {get;set;}
public IEnumerable<SelectListItem> ResidenceCountries {get;set;}
@Html.DropDownListFor(x=>x.BirthCountry,Model.BirthCountries,"") @Html.DropDownListFor(x=>x.ResidenceCountry,Model.ResidenceCountries,"")

or just have a single viewModel property which is a collection of countries and then create multiple SelectListItems in your razor view:

public IEnumerable<Country> Countries { get;set}
@Html.DropDownListFor(x=>x.BirthCountry, 
    new SelectList(Model.Countries,"countryCode","countryName", Model.BirthCountry))
@Html.DropDownListFor(x=>x.ResidenceCountry, 
    new SelectList(Model.Countries,"countryCode","countryName", Model.ResidenceCountry))

Related links

Stackoverflow question with a good answer detailing when the behaviour described above occurs

Thread on forums.asp.net discussing whether this behaviour is a bug or not

Organise ASP.MVC projects by feature by changing view location in MVC

I think it would be a safe bet to say most people stick with the default ASP.Net MVC folder structure and set up whereby the project structure is organised by architectural role (e.g. models, views and controllers) and not by business feature (e.g. registration, shopping cart, order history etc. ). For small projects to me this is fine. For bigger projects however it can be cumbersome to navigate as code related to particular features are scattered over multiple folders.

This ‘ by architectural role’ organisation goes against how in my experience software is written. Usually we build systems out feature by feature rather than writing all HTML markup, all controllers, all model classes etc. During development, this is particularly true in teams that embrace agile with its focus on delivering something that works and adds value at the end of each sprint. Imagine spending a three week sprint just writing viewModels… business value derived at end of sprint = 0. Check out this post on stackoverflow for some interesting comments on the merits of organising MVC by feature.

I’m sold… how do I organise my ASP.Net MVC project folders by feature? Well, given that MVC searches for controllers by type and name (not folder location) you can simply just start creating folders for ‘Registration’, ‘CheckOut’ etc. in the root and put your controllers, viewmodels and models in them and MVC will work fine. Views however are required to be in one of a number of predefined locations so simply creating folders won’t help you here. MVC is very extensible so we can of course change view locations and I will outline all the steps required to do this later. First however let’s look at MVC built in ‘Areas’ to see if they can meet our requirements ‘out of the box’.

Organise MVC projects using areas

Areas are around since MVC 2 and are logical groupings of controllers, models and views and other related folders (such as images, JavaScript etc.) for a particular module or feature in an MVC application.

mvcAREASA downside of areas unfortunately is that within them things are still organised into three folders; models, views and controllers. Yes everything within an area is related to a particular feature (a big step in the right direction) but code within that feature is still scattered as can be seen from the image to the left.

If this project layout gives you what you need, excellent, areas is what you should use. The extra advantage of using areas is of course they are available out of the box so if you have problems with them there is likely to be a lot of help available online. Snesh Prajapati’s article on areas in MVC 4 (no major changes in MVC 5 (not sure about MVC 6) except for how they are added I believe) on codeproject.com is one of the better articles on areas. In her article she notes the downsides of just creating moduleA, moduleB etc. folders under each of the root controllers, models and views folder. She also outlines the routing related code needed to wire up areas.

byfeature

For my recent projects areas didn’t meet my needs as I was aiming for a structure similar to the right whereby everything could be in one folder. I know I could get close to this by just putting controllers, models and viewmodels in an area root and deleting the controllers and models folders. This however means my URLs get longer (a consequence of using areas), linking between areas becomes a little more ‘complicated’ and of course I’m still forced to put my views in the views folder (albeit a feature specific one).

Steps required to change view location in MVC

Areas don’t check all my checkboxes so I needed to find out how to change the location of where MVC looks for views. If you’re interested in doing this yourself, note there are a number of steps involved (overriding the default viewEngine is not enough!!!) but all are pretty easy to follow. The steps to change view location in MVC are:

Clear existing viewEngines, add back RazorViewModel, set new locations

The first step to change view location in MVC is to remove existing view engines and add RazorViewModel back in and define where the views are. Below I’m instructing MVC to look in ~/Shared/Views/VIEWNAME.cshtml (which will be the new location for shared views) and ~/CONTROLLERNAME/VIEWNAME.cshtml for my normal views. After this MVC does not care about the default ~/Views folder anymore. Note to change view location in MVC you don’t have to create a new ViewEngine like many places recommend.

Set the new view paths in Application_Start to change view location in MVC

 

Copy old ~/Views/web.config into ~/web.config so views will work correctly

Merge the ~/Views/web.config file with into the root web.config file. Views derive from System.Web.Mvc.WebViewPage and the root web.config doesn’t have this assembly referenced by default. Referencing it from root web.config means you don’t have to add a web.config for each new feature folder you add. Not everything needs to be brought across as some of it is there already, just focus on sections highlighted in yellow below.

merge-views-config

 

Edit handlers in ~/web.config to explicitly allow JS, CSS etc. to be served

Set BlockViewHandler path to ‘*.cshtml‘ in the now updated root web.config. The web.config in the views folder had this set to ‘*’, meaning IIS would, for security reasons refuse to serve any files from the views folder. Setting it to .cshtml means all the CSS and JS will render correctly…. but wait…..

cshtml blocked

… note this is the blacklist approach where we are explicitly blocking .cshtml files from being served. We may add a new type of file in the future to a location in the project that doesn’t have it’s own web.config (and hence uses root web.config) which we do not want served but if we forget to change the above it will be silently served.

A more secure approach is to whitelist and explicitly allow whatever extension we know to be safe to be served. I’m obviously recommending the whitelist approach but I’ve included the blacklist approach just to illustrate the differences between the two as not many articles discussing how to change view location in MVC mention this when they advise the whitelist approach.

whitelist-handlers

 

Move views to new shared views folder, update _ViewStart.cshtml and tidy up

Copy all files ~/Views/Shared to ~/Shared/Views. The original views/shared folder is now a dead duck, bring whatever views (most likely _Layout.cshtml at least) you need from there to the Shared/Views folder which hangs off the root.

Copy _ViewStart.cshtml from ~/Views to ~ and update the layout path inside it to point to new location of the _Layout.cshtml file.

new viewstart

Delete existing ~/Views folder as we have everything we need from it now.

Delete controllers and models folder in root when you moved all your classes to their own feature folders.

Related Links

The above relates to MVC 4 and MVC 5. MVC 6 has not been released yet, it’s still in RC1 and RC2 is not yet scheduled so it could even be 2017 before we see a final RTM version. If you’re playing with RC1 and looking to know how to do change view locations with it, the following two links may help:

Customize ASP.NET MVC 6 View Location Easily
What’s new in MVC 6: View Location Expanders

 

matthewFor an absolutely great overview of ‘Clean Architecture‘ which is the underlying pattern behind the idea of organising software around business features please check out Matthew Renze’s excellent video on Youtube.