Forms, Repeatable Items, MVC, Validation, JSON – Phew, I got it!
Introduction
Following my last post, I want to introduce what I was really working in. A multi-item templated client-side mechanism. Fancy name, huh?
The idea is that in a form we have some single-value fields and some multi-value fields. I need both to be mapped to my DTO. That way in my Action all I have to do is map from the DTO to domain entities and save.
Let’s assume as a DTO the following classes:
public class Process
{ public Process()
{ Companies = new List<Company>();
Customers = new List<Customer>();
}
public string Number { get; set; } public IList<Company> Companies { get; set; } public IList<Customers> Customers { get; set; } }
public class Customer
{ public int Id { get; set; } }
public class Company
{ public int Id { get; set; } }
So the UI I want is somewhat like this:
(courtesy of Balsamiq Mockups – definitely worth buying)
The behavior of this UI is:
1) When the page first loads there are no companies or customers.
2) Whenever you click add company one row is added.
3) Whenever you click add customer one row is added.
4) When the user clicks Create, the form needs to be posted to an action that takes an object of type Process.
5) If the process passes validation (below), an alert should be displayed to the user.
6) If the process fails validation (below), the error messages should show in a div for the user above Process Number.
7) The validations are:
7.1) The process number needs to be filled.
7.2) At least one company must be added.
7.3) At least one customer must be added.
Preferably all of that should happen without the screen flickering (post), thus using AJAX for it.
Challenges
Ok, sounds easy, but let’s analyze some challenges imposed by this problem.
First there’s how to easily have the multi-row thing. The structure that adds new companies or customers and properly names them so they get properly bound in the MVC action. There’s another catch here. I want this to be templateable, meaning I want to use the same infra-structure for both areas of the form, just change the row template (and button text).
Then there’s the form binding (explored in my last post) so that I can save the information provided by the user. This is not trivial since the fields should be named properly, like process.Customers[0].Id and such.
Last but not least there’s the validation which should be executed using ajax. If validation passes we should show a success message.
Let’s tackle each of the challenges individually.
Multi-Value Fields
As I said before when I want to develop something for my own usage, I first try to find out how I want to use it and only then I try to build it.
So let’s see how would be the ideal way of creating this multi-value fields.
<div
class="repeater"
additemtext="Add Company"
itemTemplateSelector="#company_item_template">
</div>
That sounds good, now let’s check the template called company_item_template:
<div id="company_item_template" style="display: none;">
<table border="0" cellspacing="0" cellpadding="0" width="100%">
<tr>
<td>
Company:
</td>
<td>
<select name="Process.Companies[{index}].Id"> <option value="1">Company A</option>
<option value="2">Company B</option>
<option value="3">Company C</option>
<option value="4">Company D</option>
<option value="5">Company E</option>
</select>
</td>
</tr>
</table>
</div>
As you can see that’s all standard HTML, except for {index}. What do I mean by {index} is the row number, which is pretty obvious. This number must be filled by the client-side component so that the MVC binding can occur properly.
Ok, now that I know what I want, let’s go to how to do it.
JQuery Components
There’s a very good intro on how to do JQuery components here. I won’t dive in that subject. Suffice to say that you can extend JQuery in a way that allows you to add new functions to the JQuery Element. This allows us to enable the user to do:
$(‘div.repeater’).toRepeatable();
The code for the component is pretty self-explanatory, but if anyone wants any help with it, just leave a comment:
var NO_ITEM_TEMPLATE_ERROR_MESSAGE = 'No item templates were specified, please include an attribute with the item template element selector called \'itemTemplateSelector\' in the repeatable div!';
var repeatable_layout = '<table border="0" cellspacing="0" cellpadding="0" width="100%">';
repeatable_layout += ' <tr>';
repeatable_layout += ' <td align="right">';
repeatable_layout += ' <button class="btnAddItem">{buttonText}</button>';repeatable_layout += ' </td>';
repeatable_layout += ' </tr>';
repeatable_layout += ' <tr>';
repeatable_layout += ' <td>';
repeatable_layout += ' <div class="repeaterItems"></div>';
repeatable_layout += ' </td>';
repeatable_layout += ' </tr>';
repeatable_layout += '</table>';
jQuery.fn.toRepeatable = function(options) { $(this).each(function() { var div = $(this);
transformDivToRepeater(div, options);
bindToEventsIn(div, options);
});
function transformDivToRepeater(div, options) { var repeaterBody = (options != null && options.layout == null) ? options.layout : repeatable_layout;
var addItemText = div.attr('addItemText').toString(); repeaterBody = repeaterBody.replace("{buttonText}", addItemText == '' ? "Add" : addItemText); div.append(repeaterBody);
}
function bindToEventsIn(div, options) { var btn = div.find('button.btnAddItem'); btn.click(function() { if (div.attr('itemTemplateSelector') == null || div.attr('itemTemplateSelector') == '') { alert(NO_ITEM_TEMPLATE_ERROR_MESSAGE); return; } var item_template_selector = div.attr('itemTemplateSelector').toString(); var item_template_object = $(item_template_selector);
if (item_template_object == null || item_template_object.length == 0) { alert(NO_ITEM_TEMPLATE_ERROR_MESSAGE); return; }
var itemCount = numberOfItemsFor(div);
incrementItemCountFor(div);
var itemTemplate = item_template_object.html();
var itemsDiv = div.find('div.repeaterItems'); itemsDiv.append('<div class="item' + itemCount + '"></div>'); var currentItemDiv = itemsDiv.find('div.item' + itemCount.toString()); currentItemDiv.append(replaceCountsIn(itemTemplate, itemCount));
return false;
});
}
function replaceCountsIn(itemTemplate, itemCount) { while (itemTemplate.toString().indexOf('{index}', 0) > -1) itemTemplate = itemTemplate.toString().replace('{index}', itemCount.toString());
return itemTemplate;
}
function numberOfItemsFor(div) { return parseInt(div.attr('numberOfItems')); }
function incrementItemCountFor(div) { if (div.attr('numberOfItems') == null || div.attr('numberOfItems') == '') div.attr('numberOfItems', 1); else
div.attr('numberOfItems', parseInt(div.attr('numberOfItems')) + 1); }
};
Ok, one part down. I can do multi-value fields now. Let’s get to the binding portion.
Binding to the DTO
As outlined in my last post, the ASP.Net MVC model binder is not fully-functional. Again as I said the Monorail one is. My dear friend Claudio Figueiredo just pointed that I CAN use Castle binder. There’s an excellent blog post by Steve Gentile here on how to use it.
Suffice to say that it works as I expected and maps all my fields properly. The strategy I adopted is to have a BindingController as base class that does all the required infra-structure stuff.
Validation and Saving
All that’s left is validating and saving. I’ll first show the code for my Create action.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([CastleBind] Process process)
{ var errors = new List<string>();
if (string.IsNullOrEmpty(process.Number))
errors.Add("The process number must be filled!"); if (process.Customers.Count == 0)
errors.Add("At least one customer must be selected!"); if (process.Companies.Count ==0)
errors.Add("At least one company must be selected!");
var data = new ResultJson("Success"); if (errors.Count > 0)
{ data.Errors = errors;
data.Result = "Failure";
}
var json = new JsonResult {Data = data}; return json;
}
As you can see if any errors happen I set the result to Failure and add the error messages. Whatever happens I return a JSON representation of the result object. The JsonResult object is just a POCO:
public class ResultJson
{ public ResultJson(string result)
{ Result = result;
}
public string Result { get; set; }
public List<string> Errors { get; set; } }
What happens here is that whoever calls this action will get a JSON object as the result so that the validation errors can be shown to the user. How to do it, though?
We’ll use the JQuery.Forms plugin to post our form using AJAX. That way we can inspect the return. There’s a catch, though. We need to instruct JQuery Forms to interpret the result as JSON. This can be easily done using the following code:
var options = { dataType: 'json',
success: function(result) { if (result.Result == 'Failure') { $('#errors').html(''); for (i = 0; i < result.Errors.length; i++) { $('#errors').append('<div class="errorMessage">' + result.Errors[i] + '</div>'); }
}
else { alert('Process saved successfully.'); }
}
};
$("#form").ajaxForm(options);
The code above checks if the result is a failure, and if it is appends error messages to a div called “errors”. Otherwise just gives an alert that that process was saved.
Of course you could return more data in the result and redirect to another page or anything that you need. This is only a proof-of-concept as usual.
Conclusion
Using these structures, it’s really easy to have a complex form validated and saved in a very simple controller. What could turn into a hell hole can be solved with very very little UI Code, thus relying in the domain and strengthening the notion of Domain-Driven design.
I hope this code helps someone out there! Anyway, any doubts, suggestions, anything, just let me know!
Cheers,