Dennis Alexander
Dennis Alexander

Reputation: 861

How to Update List<Model> with jQuery in MVC 4

I am currently trying to create a settings page using a modified Index view. The goal is that the users get all settings displayed and can change all settings within one view and save all settings using one single button. The setting should be updated using Ajax.

My current approach:

View:

<script language="javascript">
    $(function() {
        $('#editSettings').submit(function () {
            if ($(this).valid()) {
                $.ajax({
                    url: this.action,
                    type: this.method,
                    data: $(this).serialize(),
                    success: function (result)
                    {
                        alert(result);                       
                    }
                });
            }
            return false;
        });
    });
</script>

[ ... ]

@using (Ajax.BeginForm("Edit", "Settings", new AjaxOptions {UpdateTargetId = "result"}, new { @class = "form-horizontal", @id = "editSettings" } ))
{
    foreach (Setting item in ViewBag.Settings) 
    {
        @Html.Partial("_SingleSetting", item)
    }
    <input type="submit" value="modify" />
}

Partial View to load the setting:

        <div class="control-group">
            <label class="control-label">@settingName</label>
            <div class="controls">
                @Html.EditorFor(model => model.Value)
                <span class="help-inline">@settingDescription</span>
            </div>
        </div>

Model:

[Table("Settings")]
public class Setting
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int SettingId { get; set; }

    public string Name { get; set; }

    [Required(AllowEmptyStrings = true)]
    [DisplayFormat(ConvertEmptyStringToNull = false)]
    public string Value { get; set; }
}

I am setting the ViewBag using ViewBag.Settings = _db.Settings.ToList();

The jQuery parses the Data to the following method:

    [HttpPost]
    public ActionResult Edit(IList<Setting> setting)
    {
        Console.WriteLine(setting.Count);
        return Content(""); // Currently for testing purposes only. Breakpoint is set to setting.Count
    }

Count throws an error because setting is null. I am quite unsure how to fix this problem.

Can someone can give me a hint?

This topic on SO already covers updating a Collection without Ajax. But I won't get the point.

Thank you for your help.

Upvotes: 3

Views: 10904

Answers (1)

Darin Dimitrov
Darin Dimitrov

Reputation: 1038810

You are using an Ajax.BeginForm and ajaxifying the form once again with jQuery. That's not necessary. But the real problem with your code is the names of the input fields in your partial. You are not respecting the naming convention used by the default model binder for binding to lists.

Let's take a complete example (removing all noise such as Entity Framework for simplicity):

Model:

public class Setting
{
    public int SettingId { get; set; }
    public string Name { get; set; }
    public string Value { get; set; }
}

Controller:

public class SettingsController : Controller
{
    public ActionResult Index()
    {
        // No idea why you are using ViewBag instead of view model
        // but I am really sick of repeating this so will leave it just that way
        ViewBag.Settings = Enumerable.Range(1, 5).Select(x => new Setting
        {
            SettingId = x,
            Name = "setting " + x,
            Value = "value " + x
        }).ToList();
        return View();
    }

    [HttpPost]
    public ActionResult Edit(IList<Setting> setting)
    {
        // Currently for testing purposes only. Breakpoint is set to setting.Count
        return Content(setting.Count.ToString()); 
    }
}

View (~/Views/Settings/Index.cshtml):

@using (Html.BeginForm("Edit", "Settings", FormMethod.Post, new { @class = "form-horizontal", id = "editSettings" }))
{
    foreach (Setting item in ViewBag.Settings) 
    {
        @Html.Partial("_SingleSetting", item)
    }
    <input type="submit" value="modify" />
}

@section scripts {
    <script type="text/javascript">
        $('#editSettings').submit(function () {
            if ($(this).valid()) {
                $.ajax({
                    url: this.action,
                    type: this.method,
                    data: $(this).serialize(),
                    success: function (result) {
                        alert(result);
                    }
                });
            }
            return false;
        });
    </script>
}

Settings partial (~/Views/Settings/_SingleSetting.cshtml):

@model Setting
@{
    var index = Guid.NewGuid().ToString();
    ViewData.TemplateInfo.HtmlFieldPrefix = "[" + index + "]";
}

<input type="hidden" name="index" value="@index" />

<div class="control-group">
    <label class="control-label">@Html.LabelFor(x => x.Name)</label>
    <div class="controls">
        @Html.EditorFor(model => model.Value)
    </div>
</div>

Notice how inside the partial it is necessary to change the HtmlFieldPrefix in order for the html helpers generate proper names for your input fields and respect the naming convention.


Alright, now let's cut the ViewCrap and do the things properly (i.e. using view models of course).

As always we start by writing a view model:

public class MyViewModel
{
    public IList<Setting> Settings { get; set; }
}

Then we adapt the controller:

public class SettingsController : Controller
{
    public ActionResult Index()
    {
        var model = new MyViewModel();

        // you will probably wanna call your database here to 
        // retrieve those values, but for the purpose of my example that
        // should be fine
        model.Settings = Enumerable.Range(1, 5).Select(x => new Setting
        {
            SettingId = x,
            Name = "setting " + x,
            Value = "value " + x
        }).ToList();
        return View(model);
    }

    [HttpPost]
    public ActionResult Edit(IList<Setting> setting)
    {
        // Currently for testing purposes only. Breakpoint is set to setting.Count
        return Content(setting.Count.ToString()); 
    }
}

View (~/Views/Settings/Index.cshtml):

@model MyViewModel

@using (Html.BeginForm("Edit", "Settings", FormMethod.Post, new { @class = "form-horizontal", id = "editSettings" }))
{
    @Html.EditorFor(x => x.Settings)
    <input type="submit" value="modify" />
}

@section scripts {
    <script type="text/javascript">
        $('#editSettings').submit(function () {
            if ($(this).valid()) {
                $.ajax({
                    url: this.action,
                    type: this.method,
                    data: $(this).serialize(),
                    success: function (result) {
                        alert(result);
                    }
                });
            }
            return false;
        });
    </script>
}

Editor template for the Settings model (~/Views/Settings/EditorTemplates/Settings.cshtml):

@model Setting
<div class="control-group">
    <label class="control-label">@Html.LabelFor(x => x.Name)</label>
    <div class="controls">
        @Html.EditorFor(model => model.Value)
    </div>
</div>

All works now by convention. No need to write any foreach loops. The @Html.EditorFor(x => x.Settings) call in the Index view analyzes the Settings property of the view model and detects that it is a collection of some other model (Setting in this case). So it will start looping through this collection and search for a corresponding editor template (~/Views/Settings/EditorTemplates/Setting.cshtml) which will automatically be rendered for each element of this collection. So you don't even need to be writing any loops in your view. And in addition to simplifying your code, now the Html.EditorFor(x => x.Value) in the editor template will generate proper names for the input field.

Upvotes: 13

Related Questions