In my previous two posts, I tried to demonstrate how to allow code to flow out of behavior specifications using mspec. In Part 1, I created the initial specification, outlining and explaining its three major components: 1) “Establish context…”, 2) “Because of…”, and 3) “It should…”. In Part 2, I focused on making initial spec and subsequent specs pass. In this post, I’d like to keep the BDD discipline alive and go a little deeper into my custom ModelBinder.
Jumping right in, our viewmodel needs to have a collection of custom fields. Each custom field has two properties: 1) an Id or Number, and 2) a value. For the sake of example, the number represents the order in which the custom fields should be processed later on.
To continue with a disciplined BDD approach, we need to write a test from which our behavior will flow. Luckily, we already built a spec Part 1 and Part 2 using Machine.Specifications (mspec for short) that we can use here. This is what we came up with:
public class when_binding_guestbook_post_data_to_viewmodel
{
static TestModelBinder _modelBinder;
static ModelBindingContext _bindingContext;
static object _result;
Establish context = () =>
{
var nameValueCollection =
new NameValueCollection
{
{ "Name", "Scott Hanselman" },
{ "Phone", "776-555-1212" }
};
_bindingContext =
new ModelBindingContext
{
ModelName = "GuestBookViewModel",
ValueProvider = new FormCollection(nameValueCollection)
};
_modelBinder =
new TestModelBinder();
};
Because of = () => _result = _modelBinder.BindModel(null, _bindingContext);
It should_not_be_null = () =>
_result.ShouldNotBeNull();
It should_be_a_guestbook_view_model = () =>
_result.ShouldBeOfType<GuestBookViewModel>();
It should_have_the_correct_name = () =>
((GuestBookViewModel)_result).Name.ShouldEqual("Scott Hanselman");
It should_have_the_correct_phone_number = () =>
((GuestBookViewModel)_result).Phone.ShouldEqual("776-555-1212");
}
The first thing I’m going to do is verify that all my specs pass. Then, I’m going to add a spec below the others:
It should_have_a_custom_fields_dictionary_that_is_not_null = () => ((GuestBookViewModel)_result).CustomFields.ShouldNotBeNull();
Passing Spec #1
Using plain english, I have specified a new behavior: “When binding guestbook post data to a viewmodel, the viewmodel should have a CustomFields dictionary that is not null.” While typing the spec, I had to add a non-existent property to the GuestBookViewModel called CustomFields. ReSharper is a great tool because it helped me add that property to my ViewModel class VERY easily. Here’s my GuestBookViewModel class:
public class GuestBookViewModel
{
public string Name { get; set; }
public string Phone { get; set; }
public Dictionary<string, string> CustomFields { get; set; }
}
Today, I’m a disciplined follower of BDD principles (no promises for tomorrow). Now that my spec is written and I can build my project, I verify that my test fails. Of course, it does. Now, I’m going to add code to make it pass!
All this spec is asking for is a non-null CustomFields property. Fair enough. The smallest unit of work I can do to make it pass is to have the ModelBinder new up a dictionary and add it to the CustomFields property.
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var vm = new GuestBookViewModel();
vm.Name = bindingContext.ValueProvider
.GetValue("Name").AttemptedValue;
vm.Phone = bindingContext.ValueProvider
.GetValue("Phone").AttemptedValue;
vm.CustomFields = new Dictionary<string, string>();
return vm;
}
Now I run the test and it passes with flying colors.
Passing Spec #2
Here’s my next spec to work from:
It should_have_a_custom_fields_dictionary_with_three_items = () => ((GuestBookViewModel)_result).CustomFields.Count.ShouldEqual(3);
As you can see, I want a dictionary of custom fields with three items. I run the spec and find, as expected, and it fails. Great! Now, let’s make it pass. The first thing I want to do is go back to the “context” section of my spec and add some items to my NameValueCollection. Before, I only passed in Name and Phone. Now, I’m going to pass in three more items, all prefixed with “CustomField”. My idea here is that my form can have N CustomFields, determined by the number of custom field items present in my post data. Here’s my modified spec context:
Establish context = () =>
{
var nameValueCollection =
new NameValueCollection
{
{ "Name", "Scott Hanselman" },
{ "Phone", "776-555-1212" },
{ "CustomField1", "red" },
{ "CustomField1", "blue" },
{ "CustomField1", "green" },
};
_bindingContext =
new ModelBindingContext
{
ModelName = "GuestBookViewModel",
ValueProvider = new FormCollection(nameValueCollection)
};
_modelBinder =
new TestModelBinder();
};
Since I’ve changed my ontext, I want to run all my specs and make sure I haven’t broken anything with my change. In this case, at least, I didn’t break anything. All my specs pass, except for the spec I’m working on right now. It is still a miserable failure. So, now, let’s go to the ModelBinder and add logic to make our current spec pass.
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var vm = new GuestBookViewModel();
vm.Name = bindingContext.ValueProvider.GetValue("Name").AttemptedValue;
vm.Phone = bindingContext.ValueProvider.GetValue("Phone").AttemptedValue;
vm.CustomFields = getDictionary("CustomField", bindingContext);
return vm;
}
private Dictionary<string, string> getDictionary(string prefix, ModelBindingContext bindingContext)
{
var dict = new Dictionary<string, string>();
foreach (var key in ((FormCollection)bindingContext.ValueProvider).AllKeys)
if (key.StartsWith(prefix))
dict.Add(key.Replace(prefix, string.Empty), ((FormCollection)bindingContext.ValueProvider)[key]);
return dict;
}
To make my code a little prettier, I decided to encapsulate my dictionary binding code into its own private class. Take another look at my spec context above. Notice that I used the NameValueCollection to instantiate a FormValueCollection which I passed into the ValueProvider property of our ModelBindingContext. In order to iterate over the items in our ValueProvider, we’ve got to cast it back to a FormValueCollection. Once you do that, it’s just like iterating over the HttpContext.Request.Form collection.
What I’m doing in the code above is I’m searching through all form values for anything starting with “CustomField”. Then I’m adding an item to the dictionary with [whatever comes after "CustomField"] as the key and the value as the value. So… now our spec should pass, right?
Success. It passed!
I hope this helps you understand how to write specs and maybe a little more about Custom ModelBinders.