ASP.NET MVC SelectList selectedValue Gotcha

by Ben Hart 24. November 2008 13:14

A recent post from Ayende was declaring a another bug in ASP.NET MVC, specifically referring to a select not rendering with the option selected. I remembered that we had had some issues some time ago, and had made our own helper to render selects, and took the opportunity to see whether we had made the same incorrect assumption - passing in the whole object, when only the value field was required.

I think we might have, but chatting to the team some other problems were experienced. It seemed the selected value was working in some situations, but not all. To keep as simple as possible, I used a test class similar to Ayende's:

public class IdName
{
    public int Id { get; set; }
    public string Name { get; set; }
}

And built a SelectList as duly instructed by the commentators on the post:

var list = new[] 
{   
    new IdName { Id = 1, Name = "Name1" }, 
    new IdName { Id = 2, Name = "Name2" }, 
    new IdName { Id = 3, Name = "Name3" } 
};
var selectList = new SelectList(list, "Id", "Name", 2);
ViewData["TestClasses"] = selectList;

Using the following extension method on the view

Html.DropDownList("TestClass", (SelectList)ViewData["TestClasses"])

this all works as expected:

<select id="TestClass" name="TestClass">
    <option value="1">Name1</option>
    <option selected="selected" value="2">Name2</option>
    <option value="3">Name3</option>
</select>

I noticed, though, that this was not working for all our views. Despite exactly the same pattern, many of our views weren't rendering the selected attribute.

At some point long ago we built our own binding framework that reflect items and passes found values through. Thus often we have a referenced object which is passed through to the view, and we there have a few helpers that get id's and appropriate 'names'. At the time this seemed fine, in retrospect we've painted ourselves into a corner. If we modified the above snippet to something along the lines of:

var list = new[] 
{   
    new IdName { Id = 1, Name = "Name1" }, 
    new IdName { Id = 2, Name = "Name2" }, 
    new IdName { Id = 3, Name = "Name3" } 
};
var selectList = new SelectList(list, "Id", "Name", 2);
ViewData["TestClasses"] = selectList;
ViewData["TestClass"] = 3;

The select does not render with the option passed in to the SelectList as the selectedValue, but rather uses what is found in ViewData with the same name as the select.

<select id="TestClass" name="TestClass">
    <option value="1">Name1</option>
    <option value="2">Name2</option>
    <option selected="selected" value="3">Name3</option>
</select>

If what is in ViewData does not correspond to the value, nothing is selected. I played around with this for much longer than I should have before investigating the beta source, which has the following block in SelectExtensions.SelectInternal():

// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (!usedViewData) {
    object defaultValue;
    if (htmlHelper.ViewData.TryGetValue(name, out defaultValue)) {
        selectList = new MultiSelectList(selectList.Items, selectList.DataValueField, selectList.DataTextField,
            (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue });
    }
}

The RenderDropDown above eventually calls this method passing in false for usedViewData, and as such the above is always executed. Thus no matter what is passed in to the selectedValue of the SelectList, it will always be overridden by any object in ViewData with the same name as the select.

Now that I understand what's happening it's a little easier to bear. I also acknowledge that one could (even should) adopt a practice of ensuring that the object in ViewData with the same key as the desired name of a select should contain value to be marked selected.

But still, if I've gone to the effort to set the selectedValue in the SelectList, I'd prefer it to take precedence.

Technorati Tags:

Currently rated 4.2 by 5 people

  • Currently 4.2/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

ASP.NET MVC

Mocking ASP.NET MVC HtmlHelper extension methods using Moq

by Ben Hart 17. October 2008 06:43

I'm in the process of upgrading our ASP.NET MVC Preview 5 app to Beta. Been quite painless so far, but hit a snag with an unmentioned change to the signature of the ViewContext class.

We've followed many by monkeypatching the HtmlHelper class further, extending it to a variety of uses. Obviously we need to test these extensions, so need a reference to an HtmlHelper instance.  We used to have the following helper method to get an HtmlHelper object:

public HtmlHelper CreateHtmlHelper(ViewDataDictionary viewData)
{
    var sw = new StringWriter();
    var rd = new RouteData();
    var tc = new TestController();
    var td = new TempDataDictionary();
    var tv = new TestView();
    var req = new HttpRequest("", "http://localhost/", "");
    var res = new HttpResponse(sw);
    var hc = new HttpContext(req, res);
    var hcw = new HttpContextWrapper(hc);
    var rc = new RequestContext(hcw, rd);
    var cc = new ControllerContext(rc, tc);
    var vc = new ViewContext(cc, "View", viewData, td);
 
    return new HtmlHelper(vc, tv);
}

I'd been aware of this method (had stumbled across it when certain tests seemed to be taking longer than they should), thought it looked pretty dodgy, ignored it, and added it to the growing list of technical debt. "Well it isn't broken..." I've joked with my teammate responsible about adding it the daily wtf, but we've both agreed we've both seen worse.

The beta of ASP.NET MVC has changed the ViewContext constructor to now require an IView and not a view name string, which fortunately broke the above, allowing me to reclaim some debt. We're using Moq, which allowed the following, much simpler, method.

public static HtmlHelper CreateHtmlHelper(ViewDataDictionary viewData)
{
    var mockViewContext = new Mock<ViewContext>(new Mock<HttpContextBase>().Object, 
                                                    new RouteData(), 
                                                    new Mock<ControllerBase>().Object, 
                                                    new Mock<IView>().Object, 
                                                    viewData,
                                                    new TempDataDictionary());
 
    var mockViewDataContainer = new Mock<IViewDataContainer>();
    mockViewDataContainer.Expect(v => v.ViewData).Returns(viewData);
 
    return new HtmlHelper(mockViewContext.Object, mockViewDataContainer.Object);
}

The HtmlHelper requires a ViewContext, and an IViewDataContainer. Mocking the ViewContext is clearly the most work, but not that dificult. In certain circumstances the HtmlHelper needs to get the ViewDataDictionary off the IViewDataContainer, so the Mock of the container above returns the one we fill with test data, which is passed into the method. This might not be necessary, depending on your situation.

If your helper extensions need more from the HttpContext (such as the Request), obviously you'll need to set those expectations too, ours currently don't. Ben Hall has an similar implementation using RhinoMocks (which would need to be changed to cater for the IView requirement), which sets expectations allowing the Resolve method to be used.

Update: I've since realised that one doesn't really 'Mock' an extension method (or any method for that matter), so don't call me out on the title!

Technorati Tags: ,,

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , ,

ASP.NET MVC | TDD

Powered by BlogEngine.NET 1.4.5.0
Theme by Mads Kristensen

About me...

I'm a passionate .NET developer, with C# my language of choice. I've been at it for a number of years now, and enjoy that I'll never shake the feeling I'm just starting out.

I love software, and I love building it even more. I love knowing that my work facilitates others', and that one line of code at a time, we're increasing our capability.

More...



Page List