Razor form with editable collection using partial

2019-07-30 16:15发布

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:

  1. 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.)

  2. 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.

  3. 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 one Item, though (rather than IEnumerable<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.

2条回答
相关推荐>>
2楼-- · 2019-07-30 16:33

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?

I think you just want to know the index within the array, and you should pass that in the ViewDataDictionary.

@for (var i = 0; i < Model.Items.Count(); i++){
  Html.RenderPartial("_ItemForm", Model.Items[i], new ViewDataDictionary(){
    { "index", i }
  });
}
查看更多
\"骚年 ilove
3楼-- · 2019-07-30 16:54

Your question pointed out a diffused lack of knowledge about how prefixes are handled in asp.net Mvc, because in 10 days none was able to propose an "accettable" solution nothwitstanding there are several ways to pass the prefix in a clean way to your partial view:

1 You may obtain the same effect of

@Html.TextBoxFor(m => m.Items[i].SomeField)

by using either DisplayFor or EditorFor to call the partial view:

@Html.DisplayFor(m => m.Items[i].SomeField)

You may either define a display/edit template with the same name of the item datatype, or simply passing the name of the partial view to the EditorFor/DisplayFor helpers. For more information see here.

2 You may also use the same overload of the RenderPartial of a previous answer to pass the complete prefix you want to use to the ViewDataDictionary by assigning it to the property: ViewDataDictionary.TemplateInfo.HtmlFieldPrefix

Once you have done this the prefix you provided will be automatically added to all input fields names.

So if you write:

@Html.TextBoxFor(m => m.Property)

the textbox will have the name: Prefix.Property, where Prefix is the Prefix you passed in

ViewDataDictionary.TemplateInfo.HtmlFieldPrefi

查看更多
登录 后发表回答