A while back I wrote a locale control for ASP.NET that gave you a nice and easy way to plug in a cascading state/province – country control into your ASP.NET websites. Now that I have moved on to using ASP.NET MVC for my new projects I needed the same control, only this time in MVC. There were aspects of the previous version of the control that I didn’t like and wanted to refactor anyways, specifically the usage of an UpdatePanel (YUCK), reliance on PostBacks, no unit tests and poor separation of concerns for the presentation, logic and persistence layers. I now present to you the brand new ASP.NET MVC version of my Locale control with all of those issues addressed.
Overview of the ASP.NET MVC Locale User Control
- ASP.NET MVC v1 User Control (Partial View)
- jQuery handles the change events and retrieves new State/Province lists via a JSON call
- Separation of concerns between the Presentation Layer, Logic Layer and Persistence Layer
- Easily customizable data source via the LocaleDao persistence class
- Unit tests with full dependency injection for all Controller and Logic methods
Basic Locale User Control Usage
Add the user control to your view:
<p><% Html.RenderPartial("LocaleUserControl"); %></p>
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %><p><label for="StatesProvinces">State / Province: </label><%= Html.DropDownList("StatesProvinces") %></p><p><label for="Countries">Country: </label><%= Html.DropDownList("Countries") %></p><script type="text/javascript">$(function() {var countries = $("#Countries");var statesprovinces = $("#StatesProvinces");countries.change(function() {statesprovinces.find('option').remove();$.getJSON('/Base/StatesProvinces', { countryId: countries.val() }, function(data) {$(data).each(function() {$("<option value=" + this.Id + ">" + this.Name + "</option>").appendTo(statesprovinces);});});});});</script>
public class BaseController : Controller{// Dependencyprotected ILogicFactory _logicFactory;public ILogicFactory LogicFactory{get { return _logicFactory ?? new LogicFactory(); }set { _logicFactory = value; }}/// <summary>/// Gets the states provinces select list./// </summary>/// <param name="country">The country to filter the states/provinces list by.</param>/// <param name="selectedStateProvince">The State/Province to select. Pass null if no default State/Province selected.</param>/// <returns></returns>public SelectList GetStatesProvincesSelectList(Country country, StateProvince selectedStateProvince){// Create the object to returnSelectList slSps = null;if (country != null){// Get the collection of states and provincesList<StateProvince> statesProvinces = LogicFactory.LocaleLogic.GetStateProvincesByCountry(country.Id);// Build a selectlist from the statesprovinces collectionif (statesProvinces != null){slSps = new SelectList(statesProvinces.Select(s => new SelectListItem{Text = s.Name,Value = s.Id.ToString()}), "Value", "Text", selectedStateProvince == null ? string.Empty : selectedStateProvince.Id.ToString());}}return slSps;}/// <summary>/// Gets the countries select list./// </summary>/// <param name="selectedCountry">The country to select. Pass null if no default country selected.</param>/// <returns></returns>public SelectList GetCountriesSelectList(Country selectedCountry){// Get the collection of countries, select United States by defaultList<Country> countries = LogicFactory.LocaleLogic.GetCountries();// Create the object to returnSelectList slCountries = null;if (countries != null){// Build a selectlist from the countries collectionslCountries = new SelectList(countries.Select(c => new SelectListItem{Text = c.Name,Value = c.Id.ToString()}), "Value", "Text", selectedCountry == null ? string.Empty : selectedCountry.Id.ToString());}return slCountries;}[AcceptVerbs(HttpVerbs.Get)]public ActionResult StatesProvinces(int countryId){// Returns JSON collection of stateprovince that are associated// with the countryId passed inList<StateProvince> sps = LogicFactory.LocaleLogic.GetStateProvincesByCountry(countryId);// Format the JSON returnreturn Json(sps.Select(s => new{s.Id,s.Name}));}}
Data Source
The data source for the locale control is an XML file that is included in the App_Data folder. You might want to put this data into your database and then use a nice ORM like NHibernate to load the data up in the persistence layer rather than relying on the XML file. This is very easy to accomplish by changing the LocaleDao class, everything else should just work after you swap out the back end.
Unit Tests
I used Microsoft Visual Studio Tests to write unit tests for all of the controller methods and logic layer methods that had any sort of logic in them. I also used Moq to mock the dependencies in the methods under test and ensure the viability of the tests. You should be able move these unit tests into your project when you use the Locale user control. If you aren’t using the Visual Studio Test framework and are using NUnit instead, it is quite easy to change the syntax.
Download
You can download the full solution including the example MVC application, locale user control and unit tests below.
4 comments:
Great tutorial! I was looking for something like this and your post came along just in time. Great Job!!!
TureMoupe
Bpgg
Thanks for this. I made a time zone offset selector adapted from your code.
However your example project when loaded into VS2010 & executed on IE8 doesn't work. The State field shows data on the first page load but when selecting another country or even going back to the initial country the states don't show. By viewing the page you can see that only the initial countries' states are in the page.
This project won't work on MVC 2.0 without the very slight modification below:-
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult StatesProvinces(int countryId)
{
// Returns JSON collection of stateprovince that are associated
// with the countryId passed in
List sps = LogicFactory.LocaleLogic.GetStateProvincesByCountry(countryId);
// Format the JSON return
return Json(sps.Select(s => new
{
s.Id,
s.Name
}),JsonRequestBehavior.AllowGet);
}
Note the JsonRequestBehavior.AllowGet
added to the return. This is the only change.
This is required due to a security issue desciribed here:
http://haacked.com/archive/2009/06/25/json-hijacking.aspx
Adding this may make your site more vulnerable.
Post a Comment