0

The current situation: For the essence of this problem, I have two database tables/models. I'm using an AJAX form to update the displayed model data in list form from each table when a new model is created and added. In effect, the page never has to reload to display any changes to the lists.

I'm displaying the create form statically for each model above the list of items. This works fine for a model that has no foreign key dependencies. However, one of my models also has a select list in the create form that is tied to the other model list on the page.

The problem: When I add a record for the simpler model, the select list for the create form of the model with the foreign key dependency does not update to include the changes. I know why it doesn't update and I think that I need to use AJAX to either recreate the create form in-place entirely or update just the select list.

Is there a way to use the AJAX form to not only update the list of the model that I'm adding an item to but also update the div that contains the create form at the same time?

I believe I have included all the relevant code to hopefully give a clearer picture of what I'm asking

Sample Create form for the simpler model:

@using (Ajax.BeginForm("_CreateCategory", new AjaxOptions()
{
    InsertionMode = InsertionMode.Replace,
    UpdateTargetId = "list-categories"
}))
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            <label class="control-label col-md-2">Name: </label>
            <div class="col-md-5">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
            <div class="col-md-5">
                <input type="submit" value="Add" class="btn btn-success" />
            </div>
        </div>
    </div>
}

Sample model with foreign key dependency

@using (Ajax.BeginForm("_CreateType", new AjaxOptions()
{
    InsertionMode = InsertionMode.Replace,
    UpdateTargetId = "list-types"
}))
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            <label class="control-label col-md-2">Name: </label>
            <div class="col-md-offset-1 col-md-6">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            <label class="control-label col-md-3">Category: </label>
            <div class="col-md-6">
                @Html.DropDownList("CategoryID", null, htmlAttributes: new { @class = "form-control" })
                @Html.ValidationMessageFor(model => model.CategoryID, "", new { @class = "text-danger" })
            </div>
            <div class="col-md-3">
                <input type="submit" value="Add" class="btn btn-success" />
            </div>
        </div>
    </div>
}

Sample list partial view for the model with the foreign key

    <div id="list-types">
        <table class="table">
            <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Category.Name)
                </th>

                <th></th>
            </tr>

        @foreach (var item in Model) {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Category.Name)
                </td>

                <td>
                    @using (Ajax.BeginForm("DeleteType", new { id = item.ID }, new AjaxOptions()
                    {
                        InsertionMode = InsertionMode.Replace,
                        UpdateTargetId = "list-types"
                    }))
                    {
                        @Html.AntiForgeryToken()
                        <button type="submit" class="no-default">
                            <span class="glyphicon glyphicon-remove"></span>
                        </button>
                    }
                </td>
            </tr>
        }

        </table>
    </div>

Controller action for adding simpler model

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult _CreateCategory([Bind(Include = "ID, Name")] Category category)
    {
        if (ModelState.IsValid)
        {
            db.Categories.Add(category);
            db.SaveChanges();
        }
        var categories = db.Categories;
        return PartialView("_ListCategories", categories.ToList());
    }

Sample Index page code that displays the lists and create forms

<div class="col-lg-4 block">
    <h3>Categories</h3>
    @Html.Action("_CreateCategory")
    @Html.Action("_ListCategories")
</div>
<div class="col-lg-4 block">
    <h3>Types</h3>
    @Html.Action("_CreateType")
    @Html.Action("_ListTypes")
</div>
Tyler Sells
  • 463
  • 4
  • 16
  • 3
    You will find this better from a performance point of view is you just use the `$.ajax()` methods and when you save a `Category`, just return a `JsonResult` indicating success (or otherwise), and in the success callback, add a new table row to the DOM, ans also add a new ` –  Oct 09 '17 at 03:27
  • Would you recommend that I keep the ajax form usage as is and just not use the standard submit button? I know that I can create a form and pass it to the $.ajax call, but I would rather not rewrite my views unnecessarily – Tyler Sells Oct 09 '17 at 04:09
  • 1
    If you do not want to solve the problem then just leave it as is :) (and what is to re-write except to replace `@Html.AjaxForm(..)` with just `
    ` and write the scripts
    –  Oct 09 '17 at 04:16
  • I think you misunderstand lol. I have no problem moving to javascript ajax. From what I've gathered, all post data should be submitted as a form. Is there anything wrong with leaving the form itself as Ajax.beginform, giving it an id, or however I end up passing it to the js ajax method, and not using the submit action associated with the ajax form by default. Or would it be better in the case to convert back to the html.beginform format and then passing that to the js ajax call? – Tyler Sells Oct 09 '17 at 04:20
  • Sorry, I don't think I saw the rest of that comment before I posted mine. I could indeed create the form manually and use some data- elements to help with the ajax method. All of the Visual Studio scaffolded stuff confuses me sometimes. I get what it does, but it's hard to tell what the best/accepted way to do things is sometimes – Tyler Sells Oct 09 '17 at 04:24
  • 1
    You do not need `@Ajax.BeginForm()` - just a `
    ` tag for using `$.ajax()`. And yes you should have a submit button (but you cancel its action) so you can test client side validation using `if('#yourFormId'').valid()` (and you do not need any `data-` attributes
    –  Oct 09 '17 at 04:26

1 Answers1

1

You wont be able to do this easily using the Ajax.BeginForm() method, and in any case, its unnecessary extra overhead to be returning the whole table again when your only adding one row to it.

Replace Ajax.BeginForm() with just <form id="createcategory"> and use $.ajax() to submit the form to create a new Category, and return its ID. In the success callback, you can then add a new row to the table and add a new <option> element to the <select> in the 2nd form

var url = '@Url.Action("_CreateCategory")'
var categorySelect = $('#CategoryID');
$('#createcategory').submit(function() {
    if (!$(this).valid()) {
        return; // exit and display validation errors
    }
    var category = $(this).find('input[name="Name"]').val(); // see notes below
    var formdata = $(this).serialize();
    $.post(url, formdata, function(result) {
        if (result.success) {
            // Add option
            categorySelect.append($('<option></option>').val(result.id).text(category));
            // Add new table row
            .... // You have not shown the view for _ListCategories but it might be somethng like
            var cell = $('<td></td>').text(category);
            var row = $('<tr></tr>').append(cell);
            $('#yourTableID').append(row);
        } else {
            .... // check for errors, update the associated placeholder generated by ValidationMessageFor()
        }
    }).fail(function (result) {
        // Oops something went wrong
    });
}

$('#createtype').submit(function() {
    // similar to above, but just add new table row based on the values of the form controls
});

and the controller method would be

[HttpPost]
[ValidateAntiForgeryToken]
public JsonResult _CreateCategory(Category category)
{
    if (ModelState.IsValid)
    {
        db.Categories.Add(category);
        db.SaveChanges();
        // Get the new ID
        int id = category.ID;
        return Json(new { success = true, id = id });
    }
    // Get the validation errors
    var errors = ModelState.Keys.Where(k => ModelState[k].Errors.Count > 0).Select(k => new { propertyName = k, errorMessage = ModelState[k].Errors[0].ErrorMessage });
    return Json(new { success = false, errors = errors });
});

A few side notes:

  1. Your view includes 2 form controls for Name so your generating duplicate id attributes which is invalid html. Consider removing the id attribute (using new { id = "" } or giving one of them a different id attribute.
  2. You do not need the [Bind] attribute, and in any case you not sending a value for ID so that should be excluded if your do keep it.
  3. Your editing data so you should be using a view model, and in the case of the 2nd model, it will contain properties int SelectedCategory and IEnumerable<SelectListItem> CategoryList and use DropDownListFor(m => m.SelectedCategory, Model.CategoryList, "Please select", new { ... }) in the view
  4. Use @Html.LabelFor(m => m.yourProperty) to correctly generate your <label> elements (a <label> is an accessibility element and clicking on it sets focus to the associated control which does not happen with your current code)
  • Thank you very much for this detailed explanation. There's a lot there I hadn't realized before. About your notes: 1. A lot of this code was scaffolded by VS. I'll inspect and see if I can figure out which controls you're talking about. 2. Why not? This was the way VS had scaffolded the create and edit methods. I had always thought that was a security thing. Is this case different because it's Ajax? And alright, I'll drop the ID binding. FYI, your explanation is much appreciated. If this comes across with a negative tone, it is unintentional. – Tyler Sells Oct 21 '17 at 14:43
  • 1
    1. You have 2 x `EditorFor(m => m.Name)` - this generates `` which means you have duplicate `id` attributes (which is invalid html) –  Oct 21 '17 at 21:58
  • 1
    2. By default, all properties are bound so using `[Bind(Include = "ID, Name")]` is just doing exactly the same thing as excluding the attribute altogether. But you only ever post back a value for `Name` anyway - you do not want the `ID` value since that will be generated when you save the `Category` so at the very least it should be just `[Bind(Include = "Name")]` to protect against a malicious user attempting to post an `ID` (and it makes no difference if its a normal submit or an ajax submit). –  Oct 21 '17 at 21:59
  • Having said that, the best approach is to always use a view model so your protected against under and over-posting attacks (as well as all the other benefits it brings). [What is ViewModel in MVC?](https://stackoverflow.com/questions/11064316/what-is-viewmodel-in-mvc) –  Oct 21 '17 at 21:59