Thursday, April 23, 2009

jQuery rocks.  It makes it really easy to do cool stuff client side with JavaScript, like these cascading DropDowns (aka Select Lists).  But at the end of the day, jQuery is JavaScript, and it can still be tricky to work with.  One of these days, Visual Studio will treat JavaScript like a first class language and life will be much easier.  You'll see that there isn't much JavaScript here, but it took me several iterations to get this right.

I'll start with the UI code and JavaScript.  If you want, you can read further and see the Controller code and more.

In my View, I have two DropDowns (Select Lists) and some JavaScript:

<p>
    Country:
    <%= Html.DropDownList("Countries", ViewData["Countries"] 
             as List<SelectListItem>) %>
</p>
<p>
    Region:
    <%= Html.DropDownList("Regions", new List<SelectListItem>()) %>
</p>

<script src="../../Scripts/jquery-1.3.2.js"></script>

<script type="text/javascript">
    $(function() {
        var countries = $("#Countries");
        var regions = $("#Regions");
        countries.change(function() {
            regions.find('option').remove();
            $.getJSON('/Home/Regions', { countryId: countries.val() }, function(data) {
                $(data).each(function() {
                    $("<option value=" + this.RegionId + ">" + this.RegionName + "</option>").appendTo(regions);
                });
            });
        });
    });
</script>

The Countries DropDown is pre-populated with data passed in ViewData. 

The second DropDown, Regions gets loaded when Countries changes.

Here's how the JavaScript and jQuery stuff works...

First I get the two Select Lists, using jQuery and the ID as the selector (jQuery makes it easy!):

        var countries = $("#Countries");
        var regions = $("#Regions");

Next I wire up the change event on the Countries list to an anonymous function (jQuery makes it easy!):

        countries.change(function() {
        // ...
            });

Inside that function I do a few things.  First, find all options in the Region Select List and remove them:

            regions.find('option').remove();

Next I need to get the data for the second Select List. Guess what?  jQuery makes this easy too.  Using the getJSON() method, I supply the Controller and Action, as well as the Id of the country so I can get the correct regions.  I also declare a function to call when the JSON result comes back:

        $.getJSON('/Home/Regions', { countryId: countries.val() }, function(data) {
        // ...
        });

The last thing to do is load all of the results into the Select List.  This actually was the hardest thing to do but in the end it is very little code.  I tried using jQuery to create options and adding them to the select list.  I tried creating a list of options and setting the html value of the Select list.  I tried a bunch of stuff.  But this worked great... Iterate over the results with jQuery's .each function and call a function for each iteration.  The function creates some dynamic html and appends it to the Region list.  I must admit, I was surprised this worked.  I'd assume that appending would put it after the select list (<select id="Regions" />) but it actually put it inside it!

        $(data).each(function() {
            $("<option value=" + this.RegionId + ">" + this.RegionName + "</option>").appendTo(regions);
        });

That's it!  It works great.  Of course there are more complex ways to do this, with caching etc.  This is just one way to accomplish the goal.

Here's some code from the Controller that helps make it work.  Just remember, this is demo code.  I'm not too worried about separation of concerns and similar details. In a production app, I'd be calling more services!

In the Index Action (in this sample the Select Lists are in my Index View) I load the countries list into ViewData.  Again, this is a demo and that may not be the best practice.

    public ActionResult Index()
    {
        ViewData["Countries"] = GetCountries();
        return View();
    }
    private List<SelectListItem> GetCountries()
    {
        List<SelectListItem> countries =
            _countryList.Select(c => new SelectListItem {Text = c.CountryName, Value = c.CountryId.ToString()}).ToList();
        countries.Insert(0, new SelectListItem{ Text="", Value = "0"});
        return countries;
    }

Then I have a Regions Action that returns the JSON result with the values to add to my Regions Select List:

    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Regions(int countryId)
    {
        return Json(_regionList.Where(r => r.CountryId == countryId).Select(r =>
                                                                            new
                                                                                {
                                                                                    r.RegionName,
                                                                                    r.RegionId
                                                                                }));
    }

 

That covers most of it.  Happy coding.

Technorati Tags: , ,
Thursday, April 23, 2009 7:30:44 PM (Eastern Standard Time, UTC-05:00)  #    Disclaimer  |  Comments [5]  |  Trackback
 Wednesday, April 22, 2009

When using strongly typed views, don't name the controller action arguments with the same name as another form value!

I've been working with ASP.NET MVC for a couple of months now.  It is really pretty awesome.  It's worked so well, I haven't had much to blog about!  The other day I ran into an issue so I'm sharing the solution.   MVC will "automagically" bind the parameters of a controller action based on the values in the form.  And if your View is strongly typed and your controller action has a parameter of that same type, it will bind it too.  But in my case, the parameter was null.  MVC wasn't loading it. And there was no exception to help me out.

Here's a simple demo, I'm not showing every line of code...

 

Typical for an MVC application, I have a view that is strongly typed:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<OrganizationRole>" %>

 

The View has a simple form which includes two text boxes:

<p>
    <label for="OrganizationName">OrganizationName:</label>
    <%= Html.TextBox("OrganizationName") %>
    <%= Html.ValidationMessage("OrganizationName", "*") %>
</p>
<p>
    <label for="Role">Role:</label>
    <%= Html.TextBox("Role") %>
    <%= Html.ValidationMessage("Role", "*") %>
</p>

 

A button on the form posts back to the controller, calling the Create Action.  Here is Action Code:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(OrganizationRole role)
{
    // Do Something Here...
    return View();
}

In the screenshot below, you can see that when I debug the app, the role parameter has not been loaded.

image

The fix was simple but it took a while to figure it out!  All I had to do was re-name the parameter from "role" to "organizationRole":

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(OrganizationRole organizationRole)
{
    // Do Something Here...
    return View();
}

You see, I was being lazy and abbreviated the argument's name.  That is ok, you can name the parameter almost anything you want.  But I gave the parameter the same name as one of the other form variables!  Notice above that my form has a text box named "Role"!  MVC tried to load the values of my parameters - it takes each of the posted values from the form ("OrganizationName" and "Role") and tries to match them up with arguments with the same names (it is not case sensitive) if they exist.  It also tries to load the entire form value (strongly typed as the class OrganizationRole) to the argument of type OrganizationRole, if one exists.  But an argument of type OrganizationRole with the name "Role" screws the who thing up!  The only problem is that it doesn't throw any exceptions.  Anyway, I changed the name and presto, it works.

Wednesday, April 22, 2009 9:01:18 PM (Eastern Standard Time, UTC-05:00)  #    Disclaimer  |  Comments [1]  |  Trackback