…meie igapäevast IT’d anna meile igapäev…

2011-05-04

ASP.NET MVC: JSON and a generic dictionary

Filed under: ASP.NET MVC — Sander @ 13:39:29
Tags: ,

If you attempt to convert a generic dictionary to JSON – with keys that are not strings nor objects, you will get an error message similar to:

‘System.Collections.Generic.Dictionary`2[[System.Int64, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]’ is not supported for serialization/deserialization of a dictionary, keys must be strings or objects.

This is a runtime error, i.e. the application will compile just fine. This is an ASP.NET MVC limitation, probably existing because in JavaScript, array keys can only be strings (foo[“bar”] = “fubar”).

I wrote a simple extension method – it simply turns your dictionary into Dictionary<string, object> using the default .ToString() method – so if you are using a complex data structure as your keys, remember to override the .ToString() method.

public static Dictionary<string, object> ToJsonDictionary<TKey, TValue>(this Dictionary<TKey, TValue> input)
{
	var output = new Dictionary<string, object>(input.Count);
	foreach (KeyValuePair<TKey, TValue> pair in input)			
		output.Add(pair.Key.ToString(), pair.Value);
	return output;
}  

Using the method is, of course, as easy as return Json(myDictionary.ToJsonDictionary());.

Generic Dictionary.AddRange() extension method.

In addition, another extension method.

For some reason, generic dictionary has gotten very little developer love from the creators of .NET, which is strange – dictionaries are one of the most widely used data structures together with the lists. Dictionaries cannot be serialized, they lack many useful methods – and one of the methods I’ve missed most is .AddRange(), ie. adding one dictionary to another.

So, another extension method:

public static void AddRange<TKey, TValue>(this Dictionary<TKey, TValue> input, Dictionary<TKey, TValue> addValues)
{
	foreach (KeyValuePair<TKey, TValue> pair in addValues)			
		input.Add(pair.Key, pair.Value);			
}

And that’s all. Simple, wasn’t it?

2011-05-02

Generic helper for ASP.NET MVC model binding

Filed under: ASP.NET MVC — Sander @ 12:41:14
Tags: , , ,

In a recent post I mentioned a small helper for model binding. The default model binder of ASP.NET MVC is fairly reasonable – it can handle input from small forms very well. But what if you have highly complex data entry forms? Or forms where additional input fields are created on the fly by JavaScript? Or very complex view models with dictionaries, lists and so forth?

You will have to write a binder of your own. It is a tedious and boring job – you have to be careful to get all the field names exactly right, allow for special cases such as (idiotic) ASP.NET MVC checkboxes and so forth. Not to forget about the possibility of user fiddling with the data – changing field names, hidden values and so forth.

One thing I heavily recommend is to keep your model binding and validation completely apart. Not only makes it the code better structured and logical, but splitting two dead boring components into two workflows allows you to fiddle more interesting things between coding them.

“Standard” modelbinder looks something like:

if (controllerContext.HttpContext.Request["Client_Address_Commune"] != null){   
	long countyCode;    
	if (long.TryParse(controllerContext.HttpContext.Request["Client_Address_Commune"], out countyCode))
		address.Commune = countyCode;
}  
if (controllerContext.HttpContext.Request["Client_Address_County"] != null){
	long countryCode;
	if (long.TryParse(controllerContext.HttpContext.Request["Client_Address_County"], out countryCode))
		address.County = countyCode;
}

As you can see, a lot of repetitive, highly boring code. So I thought – how can I reduce the repetitiveness, so I wouldn’t have to spend so much with this boring crap? (sidenote: laziness is the moving force in coding).

It has been my dream to write a better modelbinder for ASP.NET MVC – reflection-based model binder, which can handle sublasses and complex models. Alas, I haven’t had time for that – and probably never will. But I did code a simple generic binder helper:

public class BinderHelper
	{
		private readonly ControllerContext _controllerContext;
                  public BinderHelper(ControllerContext controllerContext)
		{
			_controllerContext = controllerContext;
		}
                  public T Get<T>(string keyName)
		{
			if (string.IsNullOrEmpty(_controllerContext.HttpContext.Request[keyName]))
				return default(T);   
                           var value = _controllerContext.HttpContext.Request[keyName];
                           var t = typeof(T).ToString();
                           switch (t)
			{
				case "System.Boolean":
					bool b;
					if (bool.TryParse(value, out b))
						return (T)(object)b;
					return (T)(object)(value == "on");
				case "System.Int32":
					int i;
					int.TryParse(value, out i);
					return (T)(object)i;
				case "System.Decimal":
					decimal d;
					decimal.TryParse(value, out d);
					return (T)(object)d;
				case "System.Int64":
					long l;
					long.TryParse(value, out l);
					return (T)(object)l;
				case "System.String":
					return (T)(object)(string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim());
				case "System.DateTime":
					DateTime date;
					DateTime.TryParseExact(value,
						CultureInfo.CurrentCulture.DateTimeFormat.GetAllDateTimePatterns(),
						CultureInfo.CurrentCulture,
						DateTimeStyles.AllowInnerWhite,
						out date);
					return (T)(object)date;
				default:
					throw new ApplicationException("BinderHelper doesn't know how to parse '" + t + "'.");
			}
		}
}

Using BinderHelper is very easy:

public class AddressBinder : IModelBinder
 {        
	private BinderHelper _binder;
	public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
	{    
		var  binder = new BinderHelper(controllerContext);   var model = new Address
		 {
		Commune = binder.Get<long>("Client_Address_County"),
		Country = binder.Get<long>("Client_Address_Country"),
		StreetAddress = binder.Get<string>("Client_Address_StreetAddress"),
		...                                
		};   return model;
	}
}

If your ControllerContext doesn’t have the appropriate value, BinderHelper will return the default value for the data type (return default(T); ). Otherwise it will either use TryParse() to get the value – or if parsing fails, return the default value again.

As you can see, the types supported by BinderHelper are fairly limited – that is just what I needed for my particular project. You will probably want to add double and float – and possibly char, short – or even more rarely used types, such as ulong, byte and so forth. Adding them as you need should be fairly easy using existing types as blueprint.

One thing to note is that Request[keyname] uses HttpContext.Request.Params – which “gets a combined collection of QueryString, Form, ServerVariables, and Cookies items.”. In effect, this means that item with the same key may exist in the collection many times and using Request[keyname] returns first value. If you have &startdate=2011-01-01 in your URL and you post a form which contains field named startdate with value 2011-12-31, then using Request[keyname] will get 2011-01-01, as QueryString items are before the form values.

Almost certainly you wanted the form value, not QueryString one. This is a “by design” issue of ASP.NET , not a ModelBinder bug. There is an easy fix for this, though. If it is a problem for you, modify BinderHelper not to accept ControllerContext, but NameValueCollection instead – and in your modelbinder, pass the required item collection to the BinderHelper when initializing the class. I.e. instead of ControllerContext, pass ControllerContext.HttpContext.Request.QueryString or ControllerContext.HttpContext.Request.Form NameValueCollection instead, whichever you need in that particular case. You can still use ControllerContext.HttpContext.Request.Params, if you need the combined collection.

2011-04-26

Simple ASP.NET MVC object serializer

Filed under: ASP.NET MVC — Sander @ 12:09:09
Tags: , , ,

Occasionally there is a need to give user a way to return to an exact state of the page – whether it is for giving to a co-worker or saving in bookmarks doesn’t really matter. One way is to save the state to a database – and give user ID, so we can give him an address http://…/details/1031423. Another is to provide him with an URL that contains information needed to restore the state of the page – i.e. information about the model for our ASP.NET MVC page.

There are downsides for the latter approach. For one, URL length is limited to about 2000 characters, thanks to the Internet Explorer up to version 8. IE 9 theoretically increased the limit to 4GB. Other browsers can handle far longer URL’s, so hopefully this won’t be an issue in the future.

Second problem is that it may require more work – namely, almost certainly you have to write your own modelbinder to deserialize the URL back to the object. Depending on the complexity of the object, this may be quite a lengthy task, especially as you have to verify the object as well. I will give a snippet to help with model binding in some other post.

Another downside is that long URL’s are ugly. But this is a necessary evil; a tradeoff between constantly saving lots of potentially un-needed info to the database and object serialization to GET address.

In case of a simple objects, deserialization is easy – just return from your method return RedirectToAction(“Details”, mySimpleObject); and you’re done. MVC framework is capable enough to turn your object into a querystring.

In case of very complex objects, there is a CodePlex project unbinder/unbound. It looks very neat, but I must admit I had some issues with it – also it didn’t seem to be recursive.

Why should serializer be recursive? Think of a “standard” shopping cart object – usually you will have property Client (class Person), but you may also have Payer and Recipient. Person class has one or several Address-type classes – as will your shopping cart. When you serialize your shopping cart to the URL, you want all of the above to it.

public class ModelDeserializer
{
 	private readonly RouteValueDictionary _dictionary = new RouteValueDictionary();   
	/// <summary>
	/// Recursive method
	/// </summary>
	public RouteValueDictionary ToRouteDictionary(object o, string prefix)
	 {            
	PropertyInfo[] properties = o.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
	foreach (var property in properties)
	{
		if (property.PropertyType.IsValueType || property.PropertyType.Name == "String")
		{
			var value = property.GetValue(o, BindingFlags.Public, null, null, CultureInfo.CurrentCulture);
			var defValue = property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null;
		if (value != null && !value.Equals(defValue)) //we don't need default values
		_dictionary[string.Format("{0}{1}", prefix, property.Name)] = value;
		}
		else
			ToRouteDictionary(property.GetValue(o, null), string.Concat(prefix, property.Name, "."));
	}
	return _dictionary;
	} 
}

It should be fairly self-explanatory – and you can build on this easily yourself if you need to. You call ModelDeserializer from your controller with return RedirectToAction(“Details”, new ModelDeserializer().ToRouteDictionary(myComplexModel, string.Empty)); – and will get a nice url. However, there are some things that should be pointed out:

  • ModelDeserializer in its current form handles only public instance properties. It will not give private properties nor fields. You can enable private and static properties easily by changing BindingFlags – however, that is usually not needed. You can add fields easily yourself, using properties as an example.
  • It attempts to skip properties with null or default values, to keep the URL shorter. Remember that in your modelbinder.
  • RouteValueDictionary/MVC mishandles DateTime values – it does not turn them into CultureInfo.CurrentCulture-specific string values, instead it will do en-US MM/dd/yyyy. This is ASP.NET MVC bug, so allow for that in your modelbinder! Alternately, you could add “special” handling of DateTime – ie. using something like _dictionary[string.Format(“{0}{1}”, prefix, property.Name)] = property.PropertyType.Name == “DateTime” ? ((DateTime)value).ToString() : value;, which will turn DateTime values into culture-specific string before adding them to the RouteValueDictionary, thus avoiding the bug.
  • This code will not handle collections. It is fairly easy to add, I just didn’t need that in my particular project.
  • Remember that by default my code will also serialize properties you don’t want to be serialized – passwords, for example. Simple workaround is just to have fields instead of properties for such data.

Why is the prefix needed? With recursion, it will allow you to understand from which class property the value came from. Ie. shopping cart’s client’s address zipcode name will be Client.Address.Zipcode and payer’s zipcode field will be Payer.Address.Zipcode (as in URL – …&Payer.Address.Zipcode=0010&…  – which makes it very easy to bind later.

« Eelmine lehekülg

Blog at WordPress.com.