Search code examples
ajaxrazorpartial-viewsunobtrusive-ajax

Why does Unobtrusive Ajax require a layout page (returning a partial view doesn't work)?


While creating a custom plugin, that Ajaxifies any forms that are not already Ajaxified by unobtrusive Ajax, I noticed that my partial/layout code failed to display on post-back with a normal Unobtrusive Ajax form:

For my other partial-update plugins, I conditionally render pages as partial (for ajax requests) or full with layout (for normal requests) like this:

    public ActionResult AjaxForm()
    {
        if (!Request.IsAjaxRequest())
        {
            ViewBag.Layout = "_Layout.cshtml";
        }
        return PartialView();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult AjaxForm(AjaxFormModel model)
    {
        if (!Request.IsAjaxRequest())
        {
            ViewBag.Layout = "_Layout.cshtml";
        }

        if (ModelState.IsValid)
        {
            return PartialView("Success", model);
        }

        return PartialView(model);
    }

When it came to testing a normal unobtrusive Ajax form, I used this test view, which updates just its own element UpdateTargetId="FormPanel":

@using (Ajax.BeginForm("AjaxForm", null, new AjaxOptions() { UpdateTargetId = "FormPanel" }, new { id = "FormPanel" }))
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <div class="form-group">
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.AddressLine, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.AddressLine, new { htmlAttributes = new { @class = "form-control" } })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

If I remove the test if (!Request.IsAjaxRequest()), and return the full page with master page/layout, it renders the result just fine where the form was.

Fiddler shows the "success" view is returned in either case, but it does not do anything without the layout present.

Q. What is it in a masterpage/layout that unobtrusive Ajax requires in order to extract content and replace the specified element

Update:

To make things worse it actually works correctly the first time, but not if I ajax load the same form again.


Solution

  • A: Forms must have unique IDs!

    There are loads of issues about unique IDs on SO, which browsers generally cope with, but the one you cannot ever break is having two forms loaded with the same id. Only one is visible to JavaScript/jQuery!

    My problem was caused by a duplicate form ID. You should not insert the result page back in to the form itself (leaving the form element intact).

    When I reloaded the original form, dynamically in a slideshow-style control, I wound up with duplicate form ID's.

    This meant Unobtrusive Ajax could only see the first form in the DOM!

    Solution (do not set Ajax forms to update themselves):

    Set UpdateTargetId to an element above the form, never the form itself if you load panels dynamically.

    Fix to my example view (surround form with a target panel):

    <div id="FormPanel">
        @using (Ajax.BeginForm("AjaxForm", new AjaxOptions() { UpdateTargetId = "FormPanel" }))