Search code examples
asp.net-mvcmodelbindersmodelbinder

Model Binding within a Model Binder


Firstly, bear with me here. I have a custom model binder which is successfully mapping form data to a custom object. Within this model binder it also maps form items to different custom object. What I feel I should be able to do is create a separate model binder to take care of this second mapping. This is a simplified version.

Custom objects:

public class Category
{
    public int CategoryId { get; set; }
    public string Name { get; set; }
    public string Status { get; set; }
    public string Description { get; set; }
    public IEnumerable<SubCategory> SubCategories { get; set; }
}

public class SubCategory
{
    public int CategoryId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public string Status { get; set; }
}

If my form passes back a bunch of Ids for the SubCategories, what I need to do is run off to the data repository and hydrate the SubCategory object. From the form, a list of subcategories would be submitted in the following format:

<input type="text" name="Name" value="This Category" />

<input type="hidden" name="subcat.Index" value="0" />
<select name="subcat[0].Id">
    <option value="1">Something</option>
    <option value="2">Something else</option>
</select>

<input type="hidden" name="subcat.Index" value="1" />
<select name="subcat[1].Id">
    <option value="1">Something</option>
    <option value="2">Something else</option>
</select>

<input type="hidden" name="subcat.Index" value="2" />
<select name="subcat[2].Id">
    <option value="1">Something</option>
    <option value="2">Something else</option>
</select>

Writing a custom to map the Category is obviously simple, writing the model binder which will in turn map the SubCategory (within the model binder I would run off an query my data repository) is proving a little difficult.

I am not sure how clear I have made this, apologies, thanks for reading and please let me know if there is something I can say to make this clearer!


Solution

  • My take on this is that model binders should be constructing presentation models, not entity types from your repository. The model binder should be a very simple mapping from the key/value collection of the form to a presentation model which is mostly scalar values with possibly some relationships to other types that are mostly scalar values or lists. Having to materialize entity instances from a repository adds a lot of complication, as you have found.

    Moreover, it's unnecessary. Using a presentation model has a large number of advantages, including:

    • There is never a need to whitelist the fields the user is allowed to update, since the presentation model contains only those fields.
    • The default model binder will work for all but the most complex model binding scenarios. In practice, I find that I only need to use a custom model binder when the value the user sees has to be bound to some other value in a conditional manner. When using a presentation model the structure of your presentation model should match the structure of the page, so you do not need to use a custom model binder for structural reasons.
    • You will be able to create your views and controllers before creating a database or entity model. This means you can get customer buy-in on your design before doing a large amount of the work to create the final system. This helps to sort out structural issues in the entity model before they happen. Just create a presentation model which matches the page you think the customer wants to see, build the general outline of the page using a made-up instance of this presentation model, and show it to the customer. If they're happy, you can then build the repository/entity model and write a LINQ query to map that to your presentation model.

    So in your example, the subcategories would come in from the form collection as a list of integers. Therefore, the presentation model should have the same list of integers. In the controller, after binding, you can call a method to transfer the model values from the presentation model to a materialized category instance from the repository.