This project is read-only.

Multiple ClientBlockView added with Html.RenderAction

Nov 26, 2014 at 8:22 PM
If I have multiple ClientBlockView's added to a page using multiple Html.RenderAction's then I get problems with form submission due to the fact that both client view models are added to the page using a hidden input named "$.JSonModel1".

In other words, there are two hidden inputs named "$.JSonModel1". So when I try to submit the second view module using clientSideViewModel.submit(), the MVCCT javascript actually finds the first input and submits that form instead of submitting the second form.

The problem is in BindingsHelpers.cs line #198.
Coordinator
Nov 29, 2014 at 10:01 AM
This is a problem that arise each time you use Action or RenderAction: name collides! The only way to prevent name collisione is to add a different name prefix to the different views, by placing in the view the code:

@{
Html.ViewData.TemplateInfo.HtmlFieldPrefix = Html.ViewData.TemplateInfo.HtmlFieldPrefix == null ? "newPrefix" : Html.ViewData.TemplateInfo.HtmlFieldPrefix+"."+"newPrefix"
}

You may pass the prefix in the route values of the RenderAction method, then the controller place it in the viewbag and then you take it from the ViewBag and use it as explained above.

Of course the controller that receives your submits must take into account "newPrefix". Thus for instance either the controller parameter must have the "newPrefix" name, or the model received from the submit must be placed in a property of a bigger ViewModel called "newPrefix".
Dec 19, 2014 at 3:33 PM
Edited Dec 19, 2014 at 3:36 PM
I don't think this fixes my problem because it doesn't change the name of the hidden input called "__JsonModel1". And all throughout your javascript code, the form to use is found by using $("#" + jsonHiddenId).parents('form'), which returns the wrong form.

Instead, I've come up with a bit of a hack. In MvcControlToolkit.Bindings-3.0.0.js, I've modified a couple methods to allow a form id to be passed so that the correct form is being used. So here's the new code:

    function MvcControlsToolkit_ClientViewModel_Init(viewModel, jsonHiddenId, validationType) {
        viewModel.save = function () {
            document.getElementById(jsonHiddenId).value = mvcct.utils.stringify(ko.mapping.toJS(this));
        };
        viewModel.validateAndSave = function (formId) {
            if (formId) {
                var form = $('#' + formId);
                var jsonHidden = form.find('#' + jsonHiddenId)[0];
            } else {
                var form = $('#' + jsonHiddenId).parents('form');
                var jsonHidden = document.getElementById(jsonHiddenId);
            }
            if (mvcct.unobtrusive.formIsValid(jsonHiddenId, validationType)) {
                jsonHidden.value = ko.mapping.toJSON(this);
                return true;
            }
            return false;
        }
        viewModel.saveAndSubmit = function (formId) {
            if (this.validateAndSave(formId)) {
                if (formId) {
                    var form = $('#' + formId);
                } else {
                    var form = $('#' + jsonHiddenId).parents('form');
                }
                form.submit();
            }
        };
I say this is a hack for a few reasons. First, it would obviously be better to pass the form id into the constructor. Second, validation still doesn't work because it's validating the wrong form.
Coordinator
Dec 20, 2014 at 2:41 PM
In any case your code is not correct, since Html doesn't allow two elements to have the same id (they may have the same name if they are in different forms, however).

I don't understand why the change I sufìggested doesnt work. If you use it with two different prefixes the two elements should have two different ids, say:

prefix1__JsonModel1 and prefix2____JsonModel1.
Coordinator
Dec 23, 2014 at 7:48 PM
Sorry, I understood why my solution doesnt work: the field containing the model is rendered before the partial view, and then added to the partial view form by the template compiler. This mean, you have to apply my code in the controller itself before retorning the ClientBlockPartialResult. Thie means, in turn, you have to work with the contoller ViewData. Since at that time the prefix is null it is enough:

ViewData.TemplateInfo.HtmlFieldPrefix = "newPrefix"

where you need to use a different prefix for each RenderAction
Dec 23, 2014 at 8:35 PM
This works better! However, now when I call clientViewModel.saveAndSubmit, the model being posted to the controller contains all null properties. I'm looking into it now, but do I also have to set ViewData.TemplateInfo.HtmlFieldPrefix before my page's HttpPost action starts?
Coordinator
Dec 23, 2014 at 8:53 PM
The model is.null because the model binder fails because of the prefix that was added. It should be enough to name the parameter of the action method as the prefix you added. The prefix in the View is important only if there is the risk of name collisions among all otber fields.
Dec 23, 2014 at 9:04 PM
Edited Dec 23, 2014 at 9:05 PM
Unfortunately I can't change the action method parameter name because the same action is used in multiple RenderAction calls (which means the parameter name would have to be different for each call).

In other words, I'm doing something like this:

@{ foreach(...) {
    Html.RenderAction(item)
}}
Dec 24, 2014 at 3:22 PM
I'm still not able to find a workaround for this problem.
Coordinator
Dec 24, 2014 at 3:33 PM
you have to change dynamically(from javascript) the names of all fields by removing the prefix from the names but not from the ids before submitting a form. I'll post a code snippet.
Jan 6, 2015 at 7:59 PM
Did you ever get a chance to write a code snippet for this?
Coordinator
Jan 7, 2015 at 10:05 AM
A simple solution might be changing each form input field names as soon as the form is submitted. To accomplish this you might substitute the submit button with a click button and on the click event do the following:

Pay attention the code below has not been teste so it might contain trivial errors
$(".mySubmit").click(function(){
  var button = $(this);
  var form = button.closest('form');

  if (form.validate()){//if form is valid do the stuff
     form.find('input').each(function(){//find all fields
     var field = $(this);
     var name = field.attr('name');
     name = name.substring(name.indexOf(".")+1); //and remove the first prefix(it takes just the substring after the first dot)
     field.attr('name', name);

    form.submit();
    });
 }
});