Reputation: 3632
I'm working on an application at the moment that replicates a spreadsheet like table. I've been doing it all with ASP.NET model binding and a combination of ajax and a form POST. I've run into some issues where I think I might be better off creating a custom model binder.
Here's my application:
What we have there are stages of a project, and then a bunch of columns corresponding to each month with the predicted cashflows.
There is a button to the right of that image that lets you add an extra month column, and you can add a stage using the add stage button. Those two are done with AJAX, and then when you submit the form I want it to save all changes in one go via a POST.
I've managed to manoeuvre my way around using HTML helpers, view models and default model binding to get it working with adding columns:
In my main view, I have this to generate the rows/columns in the TBODY
<tbody>
@for (int stageIndex = 0; stageIndex < Model.Stages.Count; stageIndex++)
{
@Html.EditorFor(x => x.Stages[stageIndex])
}
</tbody>
<tfoot>
I have a partial view for CashflowStageViewModel which currently looks like:
@model BWInvoicing.Ui.Models.CashflowStageViewModel
<tr class="stageRow">
@Html.HiddenFor(x => x.Stage)
<td class="stage">
<div class="stageWrapper">
<div>
@Model.Stage
</div>
<div class="stageOptions">
<img class="deleteRowButton" src="../../../Content/Images/delete.png" [email protected] />
</div>
</div>
</td>
<td>
@Html.TextBoxFor(x => x.Amount, new { @Class = "totalFee emTotal", CashflowStage = Model.Stage })
</td>
<td>
@Html.TextBox("summation", 0, new { @Class = "emTotal summation", CashflowStage = Model.Stage, @readonly = "true" })
</td>
@for (int i = 0; i < Model.Months.Count; i++)
{
<td>
@Html.HiddenFor(x => Model.Months[i].Date)
@Html.TextBoxFor(x => Model.Months[i].Amount, new { @Class = "prediction", CashflowStage = Model.Stage })
</td>
}
<td class="stageHeadingEnd">
@Model.Stage
</td>
</tr>
The model binding works by creating IDs and name's that look like this:
id="Stages_5__Months_4__Amount" name="Stages[5].Months[4].Amount"
This causes some issues as I can add/delete rows between the time that these are generated and the time they are sent back to the server. I always try to keep using the default binding, but now it's holding me back so I think it's time I just created a custom model binder, rather than try dirty hacks to keep the indexes right.
My questions are these:
By this I mean, for each cell should I have a hidden with name of STAGENAME_DATE and value of whatever they enter in the text box? and then I can have a model binder split those strings up to split the stage name and the date, parse them, and assign a value to the stage on that certain date? Or is there a more appropriate way of doing it?
Upvotes: 0
Views: 1432
Reputation: 3632
I ended up reading about how you can use your own arbritrary indexes here: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
Non-Sequential Indices Well that’s all great and all, but what happens when you can’t guarantee that the submitted values will maintain a sequential index? For example, suppose you want to allow deleting rows before submitting a list of books via JavaScript. The good news is that by introducing an extra hidden input, you can allow for arbitrary indices. In the example below, we provide a hidden input with the .Index suffix for each item we need to bind to the list. The name of each of these hidden inputs are the same, so as described earlier, this will give the model binder a nice collection of indices to look for when binding to the list.
As a result I updated my viewmodel output to be following:
@model BWInvoicing.Ui.Models.CashflowStageViewModel
<tr class="stageRow">
@* Setup custom indexer to allow for add/remove rows *@
@Html.Hidden("Index", Model.Stage)
@* Store stage to rebuild view model, use bracket notation to allow default model binder to work correctly *@
@Html.Hidden("[" + Model.Stage + "].Stage",
Model.Stage)
<td class="stage">
<div class="stageWrapper">
<div>
@Model.Stage
</div>
<div class="stageOptions">
<img class="deleteRowButton" src="../../../Content/Images/delete.png" [email protected] />
</div>
</div>
</td>
<td>
@Html.TextBox("[" + Model.Stage + "].Amount", Model.Amount,
new { @Class = "totalFee emTotal", CashflowStage = Model.Stage })
</td>
<td>
@* Readonly javascript display of summation *@
@Html.TextBox("summation", 0,
new
{
@Class = "emTotal summation",
CashflowStage = Model.Stage,
@readonly = "true"
})
</td>
@* Output the month inputs, setup the correct indexes *@
@for (int i = 0; i < Model.Months.Count; i++)
{
<td>
@Html.Hidden("[" + Model.Stage + "].Months[" + i + "].Date",
Model.Months[i].Date)
@Html.TextBox("[" + Model.Stage + "].Months[" + i + "].Amount",
Model.Months[i].Amount, new { @Class = "prediction", CashflowStage = Model.Stage })
</td>
}
<td class="stageHeadingEnd">
@Model.Stage
</td>
</tr>
I'm not very happy with how messy the concatenation is, but it works pretty well.
Upvotes: 0
Reputation: 1038830
I would recommend you taking a look at the Steven Sanderson's excellent article on this matter. He uses a custom Html.BeginCollectionItem
helper which takes care of generating proper names of the input fields so that you don't have to worry about the binding. The default model binder will work out of the box.
Upvotes: 3