views:

213

answers:

1

Hi,

In some special cases you will need a list of textboxes (to deal with n - n associations) whose id is not know before runtime. Something like this : http://screencast.com/t/YjIxNjUyNmU

In that particular sample I'm looking to associate a count to some of my 'templates'.

in ASP.Net MVC 1 I coded a Dictionary ModelBinder to have a clean and intuitive HTML. It allowed things like this :

// loop on the templates
foreach(ITemplate template in templates)
{
        // get the value as text
        int val;
        content.TryGetValue(template.Id, out val);
        var value = ((val > 0) ? val.ToString() : string.Empty);

        // compute the element name (for dictionary binding)
        string id = "cbts_{0}".FormatMe(template.Id);
%>
        <input type="text" id="<%= id %>" name="<%= id %>" value="<%= value %>" />
        <label for="<%= id %>"><%= template.Name %></label>
        <br />

Here is the code of the binder :

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    IDictionary<int, int> retour = new Dictionary<int, int>();

    // get the values
    var values = bindingContext.ValueProvider;
    // get the model name
    string modelname = bindingContext.ModelName + '_';
    int skip = modelname.Length;

    // loop on the keys
    foreach(string keyStr in values.Keys)
    {
        // if an element has been identified
        if(keyStr.StartsWith(modelname))
        {
            // get that key
            int key;
            if(Int32.TryParse(keyStr.Substring(skip), out key))
            {
                int value;
                if(Int32.TryParse(values[keyStr].AttemptedValue, out value))
                    retour.Add(key, value);
            }
        }
    }
    return retour;
}

When passing to ASP.Net MVC 2, the problem is the ValueProvider is not a dictionary anymore. There is no way to loop through the values to parse them the way I did.

And I did not find any other way to do so (If you know one, tell me).

I finally switched to the 'standard' way of binding the dictionary, but the HTML is ugly, counterintuitive (using counters to loop over a non-indexed collection ??) and all the values are required, unlike the behavior I need (and that worked perfectly in ASP.Net MVC 1).

It looks like this :

int counter= 0;
// loop on the templates
foreach(ITemplate template in templates)
{
        // get the value as text
        int val;
        content.TryGetValue(template.Id, out val);
        var value = ((val > 0) ? val.ToString() : string.Empty);

        // compute the element name (for dictionary binding)
        string id = "cbts_{0}".FormatMe(template.Id);
        string dictKey = "cbts[{0}].Key".FormatMe(counter);
        string dictValue = "cbts[{0}].Value".FormatMe(counter++);
%>
        <input type="hidden" name="<%= dictKey %>" value="<%= template.Id %>" />
        <input type="text" id="<%= id %>" name="<%= dictValue %>" value="<%= value %>" />
        <label for="<%= id %>"><%= template.Name %></label>
        <br />

In the controller I need to trick the ModelState to avoid 'a value is required' errors :

public ActionResult Save(int? id, Dictionary<int, int> cbts)
{
    // clear all errors from the modelstate
    foreach(var value in this.ModelState.Values)
        value.Errors.Clear();

This is too tricky. I will soon need to use this kind of binding many times, and maybe have some tiers developer work on the application.

Question :

  • Do you know a way to make it better ?

What I need, IMO, is a ModelBinder for dictionary that allow a better html and that does no consider that all values are required.

+2  A: 

You are absolutely free to continue using your MVC 1 model binder in MVC 2. The biggest change that you'll have to make is that your model binder shouldn't go against the IValueProvider but should instead go directly against Request.Form. You can enumerate over that collection and perform whatever logic is necessary for your particular scenario.

The purpose of the IValueProvider interface is to provide an abstraction over where the data comes from and is meant for a general-purpose binder (like the DefaultModelBinder). As the data isn't guaranteed to be enumerable, the IValueProvider itself cannot be IEnumerable. But in your particular case, where you have a specific binder for a specific scenario and already know the data is coming from the form, the abstraction is unnecessary.

Levi
Thanks for your advice. Sometime going back to basis is the key, and it's so obvious you can't see it...
Mose