So, the title is a little contrived, but this is essentially what I want to accomplish:
<ul data-bind="template: { name: 'ItemFormTemplate', foreach: Items }">
@foreach (var item in Model.Items)
{
Html.RenderPartial("_ItemForm", item);
}
</ul>
<script type="text/html" id="ItemFormTemplate">
@{ Html.RenderPartial("_ItemForm", new Item()); }
</script>
The idea here is that:
I want the form for
Item
to be a partial and I want to use the same partial for both Razor's foreach and feeding Knockout a template. (I don't want to repeat the HTML or have to create a special version just for Knockout.)The Razor foreach is for fallback in case JavaScript is disabled or not supported. The form should still be editable and submittable (as in the model binder will process the POST data correctly) with or without JavaScript.
This of course means that the field names will need to follow the a pattern like
Items[0].SomeField
. The partial will be strongly typed for just oneItem
, though (rather thanIEnumerable<Item>
). Understandably this won't really be possible for the Knockout template, since the current index will only be available client-side. I have a workaround I'm already using for that:ko.bindingHandlers.prefixAndIndex = { init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { if (bindingContext.$index) { var prefix = ko.utils.unwrapObservable(valueAccessor()); var $element = $(element); $element.find('input,textarea,select').each(function () { var $this = $(this); var newId = prefix + '_' + bindingContext.$index() + '__' + $this.attr('id'); var newName = prefix + '[' + bindingContext.$index() + '].' + $this.attr('name'); $('label[for=' + $this.attr('id') + ']').attr('for', newId); $this.attr('id', newId); $this.attr('name', newName); }); } } };
This just binds to the top-level element of the template (the
<li>
in this case) and searches and replaces all id and name attributes with the proper prefix. It seems to work pretty well, but any critiques or suggestions for improvement welcome.
Really, the only thing I'm struggling with right now is getting the proper prefix into the partial when inside the Razor foreach. Typically you'd solve this using a for
instead of foreach
and then passing the indexed item instead of just a generic item, i.e.:
@for (var i = 0; i < Model.Items.Count(); i++)
{
@Html.TextBoxFor(m => m.Items[i].SomeField)
}
But, this doesn't work when using a partial, because once you're into the partial, you're still just dealing with a single Item
, out of context.
So, the most pressing question is how can I still use a partial strongly-type to Item
, but still have it be aware of any index present? Then, aside from that, how to best reduce duplication or HTML and logic (increase code reuse). Am I on the right track with the rest of my thinking. Is there anything I can improve or do better?
EDIT
I should mention that ideally I want to keep the partial strongly-typed and actually take advantage of that. In other words, I want to be able to do @Html.TextBoxFor(m => m.SomeField)
rather than faking it like @Html.TextBox("Items[" + iterator + "].SomeField")
. If I have to go that route, that's one thing, but I'm hoping there's a better way.
I suppose I could also do something along the lines of:
@Html.TextBox(prefix + "[" + iterator + "]." + Html.NameFor(m => m.SomeField))
And then pass prefix
and iterator
into the partial. That's better in the sense that I'm not having to deal with hard-coded values, but it's a lot of extra logic going on especially for a partial with a bunch of fields. Again, hoping for a better way.