Dec 272013
 

I learn something every day whether I like it or not. Today’s lesson:

SelectList thinks she’s smarter than you.

Observations

I was working in an MVC4 app, making some forms light up with some custom HtmlHelpers. Everything is dandy until a drop-down doesn’t re-populate with the previously selected value after a POST or a fresh GET. That’s funky. The right value is in the database. So I looked at the .cshtml. I had two drop-downs next to each other. I changed the custom HtmlHelpers to PODDLFs (plain old DropDownListFor) and it does the same thing. The one for Suffix “binds” the previously selected value as I’d expect, but the one for Title appears to do nothing.

@Html.DropDownListFor(model => model.Title, Model.SelectLists.PreferredTitles)
@Html.DropDownListFor(model => model.Suffix, Model.SelectLists.Suffixes)

So to be safe, let’s print out the value of Title as a string literal.

Testing: @Model.Title

Yep, works fine. I see “Mr.” just as I’d expect. So I searched for every instance of “.Title” to see if this is happening somewhere else in the app, but there are no other uses in a .cshtml file. What I did find was many instances of @ViewBag.Title being used to set the window and page titles throughout the app. I renamed “Title” to “Prefix” on the model and the fog clears a little. There’s something going on with ViewBag’s Title taking precedence over my model’s Title. To be sure, I undid the renaming operation and changed the impacted view’s ViewBag.Title to be “Mr.”, and then “Dr.”. Regardless of the current value of Model.Title, the value of ViewBag.Title is always used to set the selected value.

Analysis

You can build your SelectList and set whatever “selectedValue” you want. DropDownListFor calls SelectInternal (excerpt below) to build the MvcHtmlString. SelectInternal is responsible for binding the appropriate value for the model/property used in the expression of DropDownListFor.  When the value is not found with GetModelStateValue, ViewData.Eval is used to get the “selected value”. Deep in the internals of ViewData.Eval, ViewBag takes precedence over your model.

object defaultValue = allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string[])) : htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string));
if ((!flag && (defaultValue == null)) && !string.IsNullOrEmpty(name))
{
    defaultValue = htmlHelper.ViewData.Eval(name);
}
if (defaultValue != null)
{
    selectList = GetSelectListWithDefaultValue(selectList, defaultValue, allowMultiple);
}

So what actually happened was SelectInternal took my page title and tried to make it the selected value in the drop-down list. Knowing why it does this doesn’t make me any happier. I’d really prefer that DropDownListFor use my model’s value like I told it to. Alas, I didn’t write this code and it’s pretty dumb of me to not recognize the clear naming conflict. So I’ll accept this and move on.

Corrective Action

Clearly the best solution is to use much more descriptive names that don’t clobber each other. Changing ViewBag.Title to be ViewBag.PageTitle is the path of least resistance. Simply using “Title” on the model wasn’t very good either. It would be better as “Salutation”, “NamePrefix” or “PreferredTitle” anyway. These types of hidden naming conflicts are sure to stump some people. Remembering this little nugget of the SelectList internals will keep naming/conflicts on my mind for some time.

Aug 292011
 

You will find countless blog posts, forum posts, and Stack Overflow questions concerning the topic of unit testing a ASP.NET MVC HTML Helpers. Unit testing is an art, and I am still a novice. I played around with NUnit between 2004 and 2006. I really enjoyed practicing TDD but couldn’t make it work in my 9-5 job. Finding people that are trained TDD’ers is near impossible in .NET (or maybe just in Jacksonville). Finding people that want to learn/love TDD is just as hard. It is especially difficult to convince management to move toward TDD when they see a huge upfront cost with no perceived benefit to the business/client/users. If your shop is cranking out low-defect code already, it’s a really hard sell.

In 2008, I changed jobs and started at a company that seemed to value TDD as much as I did. A new greenfield project was starting up, and everyone was tasked with learning the tools and trade of TDD. Sadly, I was not on this project. The team spent weeks, many weeks, downloading mocking frameworks and experimenting with other unit testing frameworks. They tried Moq, Rhino Mocks, Moles, TypeMock, and others. They tried NUnit, TypeMock, and MSTest. Without a TDD expert, the team spent way too much time trying to figure out the right way to test and lost track of the project’s goal… to write code to fulfill a contract. All unit testing was soon forbidden in the interest of time and money.

In my opinion, a single bad test is still better than no tests. Since that event I have tried to avoid using other frameworks and mocking libraries, etc. Sure it makes it easier, but only if you have the time to get up to speed on it and learn to love it. My primary goal on every piece of software I write is to one day be able to walk away and never get a phone call. If I lock a development team into using version 1.2.34.5678 of Crazy Mocks, they are going to 1) hate me 2) remove the test project 3) call me. I don’t want any of that to happen. I want easy-to-read code that looks like code that is versioned with the rest of the code and uses a built-in testing framework like MSTest.

So, what am I saying here and what does it have to do with MVC and fakes? I want to unit test all my code, and testing MVC HTML helpers is hard because there are lots and lots of MVC framework stuff going on that we don’t see. Simply calling your HTML helper’s methods without building the Controller, ControllerContext, HttpContext, ViewContext, ViewDataContainer, RouteData, and ViewEngine needed to support that call will give you mixed results. If your HTML helper is simple enough, you may never need to fake the view engine and context objects. If your helper is a container of built-in System.Web.Mvc.Html helpers, you are in for a tough battle. You will find many solutions to unit test HTML helpers that use Moq or Rhino Mocks. I find that developers generally accept unit tests as being worthy of the effort. They know the benefits and the costs, and try their best. When you get into a sticky situation as with MVC helpers, many give up and the code ends up having no unit tests.

A typical error message you will see is:

System.NotImplementedException – The method or operation is not implemented.

   at System.Web.HttpContextBase.get_Items()
   at System.Web.Mvc.Html.TemplateHelpers.GetActionCache(HtmlHelper html)
   at System.Web.Mvc.Html.TemplateHelpers.ExecuteTemplate(HtmlHelper html, 
      ViewDataDictionary viewData, String templateName, DataBoundControlMode mode, 
      GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions)
   at System.Web.Mvc.Html.TemplateHelpers.TemplateHelper(HtmlHelper html, 
      ModelMetadata metadata, String htmlFieldName, String templateName, 
      DataBoundControlMode mode, Object additionalViewData, ExecuteTemplateDelegate executeTemplate)
   at System.Web.Mvc.Html.TemplateHelpers.TemplateHelper(HtmlHelper html, 
      ModelMetadata metadata, String htmlFieldName, String templateName, 
      DataBoundControlMode mode, Object additionalViewData)
   at System.Web.Mvc.Html.TemplateHelpers.TemplateFor[TContainer,TValue](HtmlHelper`1 html, 
      Expression`1 expression, String templateName, String htmlFieldName, 
      DataBoundControlMode mode, Object additionalViewData, TemplateHelperDelegate templateHelper)
   at System.Web.Mvc.Html.TemplateHelpers.TemplateFor[TContainer,TValue](HtmlHelper`1 html, 
      Expression`1 expression, String templateName, String htmlFieldName, DataBoundControlMode mode, 
      Object additionalViewData)
   at System.Web.Mvc.Html.DisplayExtensions.DisplayFor[TModel,TValue](HtmlHelper`1 html, 
      Expression`1 expression)
   at Utils.Web.Tests.DisplayFormRowHelperTest.DisplayFormRow_StringField()
   in C:DevMvcUnitTestingUtils.Web.TestsDisplayFormRowHelperTest.cs:line 99

The ActionCacheItems are stored as a dictionary in the HttpContext.Items. If you have successfully used Moq and NSubstitute correctly and mocked away a good part of the framework, you may still see this error because the HttpContext hasn’t been built up correctly.

What can you do? Find or write some fakes. Then create your HTML helper with a method that builds up the view engine and context objects as seen in the code below:

using System.Web.Mvc;
using System.Web.Routing;
using UnitTesting.Web.Mvc;

namespace Utils.Web.Tests
{
	public class HtmlHelperBuilder
	{
		public static HtmlHelper<TModel> GetHtmlHelper<TModel>(TModel model, bool clientValidationEnabled)
		{
			ViewEngines.Engines.Clear();
			ViewEngines.Engines.Add(new FakeViewEngine());

			var controller = new MyTestController();
			var httpContext = new FakeHttpContext();

			var viewData = new FakeViewDataContainer { ViewData = new ViewDataDictionary<TModel>(model) };

			var routeData = new RouteData();
			routeData.Values["controller"] = "home";
			routeData.Values["action"] = "index";

			ControllerContext controllerContext = new FakeControllerContext(controller);

			var viewContext = new FakeViewContext(controllerContext, "MyView", routeData);
			viewContext.HttpContext = httpContext;
			viewContext.ClientValidationEnabled = clientValidationEnabled;
			viewContext.UnobtrusiveJavaScriptEnabled = clientValidationEnabled;
			viewContext.FormContext = new FakeFormContext();

			HtmlHelper<TModel> htmlHelper = new HtmlHelper<TModel>(viewContext, viewData);
			return htmlHelper;
		}
	}
}

Then your unit tests will work, and not look so obnoxious. If you compare the code below, dear blog reader with a head on your shoulders, to the Moq solutions out there, you will notice two things. 1) This looks like a lot of code, but it’s still less than Moq 2) You, and a lot of other people, can actually read this code. Moq is great, but not for most developer’s consumption.

[TestMethod]
public void DisplayFormRow_StringField()
{
	// Arrange
	var model = new MyModel { MyString = "Test" };
	string expected = "Test";
	HtmlHelper<MyModel> html = HtmlHelperBuilder.GetHtmlHelper(model, true);

	// Act
	MvcHtmlString actual = html.DisplayFor(m => m.MyString);

	// Assert
	Assert.AreEqual(expected, actual.ToHtmlString());
}

This should be enough fake action to get someone going down the right path. Feel free to use the fakes library I have linked below. No guarantees. IWOMB

Code hard!
Test lightly!

Links:

Download library of fakes – UnitTesting.Web.Mvc.zip (8.42 KB)