35

I'm using MVC 4 and Entity Framework to develop an intranet web application. I have a list of persons which can be modify by an edit action. I wanted to make my app more dynamic by using modal forms. So I tried to put my edit view into my Bootstrap modal and I have 2 questions about it :

  • Should I use a simple or a partial view?
  • How can I perform the validation (actually it work but it redirects me to my original view so not in the modal form)

I think I have to use AJAX and/or jQuery but I'm new to these technologies. Any help would be appreciated.

EDIT : My Index View :

@model IEnumerable<BuSIMaterial.Models.Person>

@{
    ViewBag.Title = "Index";
}


<h2>Index</h2>

<br />

<div class="group">
        <input type="button" value="New person" class="btn" onclick="location.href='@Url.Action("Create")';return false;"/>
        <input type="button" value="Download report" class="btn" onclick="location.href='@Url.Action("PersonReport")';return false;"/>
</div>


@using (Html.BeginForm("SelectedPersonDetails", "Person"))
{  
    <form class="form-search">
        <input type="text" id="tbPerson" name="tbPerson" placeholder="Find an employee..." class="input-medium search-query">
        <button type="submit" class="btn">Search</button>
    </form>
}


<table class="table">
    <thead>
        <tr>
            <th>Firstname</th>
            <th>Lastname</th>
            <th>Start Date</th>
            <th>End Date</th>
            <th>Details</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
@foreach (BuSIMaterial.Models.Person item in ViewBag.PageOfPersons)
{
    <tr>
        <td>@item.FirstName</td>
        <td>@item.LastName</td>
        <td>@item.StartDate.ToShortDateString()</td>
        <td>
            @if (item.EndDate.HasValue)
            {
                @item.EndDate.Value.ToShortDateString()
            }        
        </td>

        <td>
            <a class="details_link" data-target-id="@item.Id_Person">Details</a>
        </td>

        <td>
            <div>
                <button class="btn btn-primary edit-person" data-id="@item.Id_Person">Edit</button>

            </div>
        </td>

    </tr>
    <tr>
        <td colspan="6">

            <table>
                <tr>
                    <th>National Number</th>
                    <td>@item.NumNat</td>
                </tr>
                <tr>
                    <th>Vehicle Category</th>
                    <td>@item.ProductPackageCategory.Name</td>
                </tr>
                <tr>
                    <th>Upgrade</th><td>@item.Upgrade</td>
                </tr>
                <tr>
                    <th>House to work</th>
                    <td>@item.HouseToWorkKilometers.ToString("G29")</td>
                </tr>
            </table>
            <div id="details_@item.Id_Person"></div>
        </td>

    </tr>

}
    </tbody>
</table>

<div class="modal hide fade in" id="edit-member">
    <div id="edit-person-container"></div>
</div>

@section Scripts
{
    @Scripts.Render("~/bundles/jqueryui")
    @Styles.Render("~/Content/themes/base/css")

    <script type="text/javascript" language="javascript">

        $(document).ready(function () {

            $('#tbPerson').autocomplete({
                source: '@Url.Action("AutoComplete")'
            });

            $(".details_link").click(function () {
                var id = $(this).data("target-id");
                var url = '/ProductAllocation/ListByOwner/' + id;
                $("#details_"+ id).load(url);
            });

            $('.edit-person').click(function () {
                var url = "/Person/EditPerson"; 
                var id = $(this).attr('data-id');
                $.get(url + '/' + id, function (data) {
                    $('#edit-person-container').html(data);
                    $('.edit-person').modal('show');
                });
            });


        });

    </script>
}

My Partial View :

@model BuSIMaterial.Models.Person

<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
    <h3 id="myModalLabel">Edit</h3>
</div>
<div>

@using (Ajax.BeginForm("EditPerson", "Person", FormMethod.Post,
                    new AjaxOptions
                    {
                        InsertionMode = InsertionMode.Replace,
                        HttpMethod = "POST",
                        UpdateTargetId = "list-of-people"
                    }))
{

    @Html.ValidationSummary()
    @Html.AntiForgeryToken()
    <div class="modal-body">
        <div class="editor-field">
            @Html.TextBoxFor(model => model.FirstName, new { maxlength = 50 })
            @Html.ValidationMessageFor(model => model.FirstName)
        </div>

        <div class="editor-field">
            @Html.TextBoxFor(model => model.LastName, new { maxlength = 50 })
            @Html.ValidationMessageFor(model => model.LastName)
        </div>
    </div>
    <div class="modal-footer">
        <button class="btn btn-inverse" type="submit">Save</button>
    </div>
}
Traffy
  • 2,803
  • 15
  • 52
  • 78
  • hi traffy, have you done how to edit modal form using bootstrap? I have same problem, could you update or post the complete code? – Willy Apr 28 '14 at 11:36

4 Answers4

53

You should use partial views. I use the following approach:

Use a view model so you're not passing your domain models to your views:

public class EditPersonViewModel
{
    public int Id { get; set; }   // this is only used to retrieve record from Db
    public string Name { get; set; }
    public string Age { get; set; }
}

In your PersonController:

[HttpGet] // this action result returns the partial containing the modal
public ActionResult EditPerson(int id)
{  
    var viewModel = new EditPersonViewModel();
    viewModel.Id = id;
    return PartialView("_EditPersonPartial", viewModel);
}

[HttpPost] // this action takes the viewModel from the modal
public ActionResult EditPerson(EditPersonViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        var toUpdate = personRepo.Find(viewModel.Id);
        toUpdate.Name = viewModel.Name;
        toUpdate.Age = viewModel.Age;
        personRepo.InsertOrUpdate(toUpdate);
        personRepo.Save();
        return View("Index");
    }
}

Next create a partial view called _EditPersonPartial. This contains the modal header, body and footer. It also contains the Ajax form. It's strongly typed and takes in our view model.

@model Namespace.ViewModels.EditPersonViewModel
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
    <h3 id="myModalLabel">Edit group member</h3>
</div>
<div>
@using (Ajax.BeginForm("EditPerson", "Person", FormMethod.Post,
                    new AjaxOptions
                    {
                        InsertionMode = InsertionMode.Replace,
                        HttpMethod = "POST",
                        UpdateTargetId = "list-of-people"
                    }))
{
    @Html.ValidationSummary()
    @Html.AntiForgeryToken()
    <div class="modal-body">
        @Html.Bootstrap().ControlGroup().TextBoxFor(x => x.Name)
        @Html.Bootstrap().ControlGroup().TextBoxFor(x => x.Age)
    </div>
    <div class="modal-footer">
        <button class="btn btn-inverse" type="submit">Save</button>
    </div>
}

Now somewhere in your application, say another partial _peoplePartial.cshtml etc:

<div>
   @foreach(var person in Model.People)
    {
        <button class="btn btn-primary edit-person" data-id="@person.PersonId">Edit</button>
    }
</div>
// this is the modal definition
<div class="modal hide fade in" id="edit-person">
    <div id="edit-person-container"></div>
</div>

    <script type="text/javascript">
    $(document).ready(function () {
        $('.edit-person').click(function () {
            var url = "/Person/EditPerson"; // the url to the controller
            var id = $(this).attr('data-id'); // the id that's given to each button in the list
            $.get(url + '/' + id, function (data) {
                $('#edit-person-container').html(data);
                $('#edit-person').modal('show');
            });
        });
     });
   </script>
joshmcode
  • 3,471
  • 1
  • 35
  • 50
MattSull
  • 5,514
  • 5
  • 46
  • 68
  • Thanks for your answer. However, why should I use a ViewModel instead of my initial model? – Traffy Apr 15 '13 at 09:33
  • 1
    There's a number of reasons, check out this answer for an explanation http://stackoverflow.com/questions/4878937/why-should-i-use-view-models If you need any help with my answer just ask. – MattSull Apr 15 '13 at 09:39
  • 5
    Thank you very much ! Indeed I have a little question : in your Ajax.Begin form, what does the "list-of-people" represent? – Traffy Apr 15 '13 at 09:39
  • 1
    You might have a DIV in your app that contains a list of people with id="list-of-people". UpdateTargetId is the DIV/element you want to update with the new data from the Ajax form i.e. a person. Just focus on getting the modal window to open first, then work with the form. – MattSull Apr 15 '13 at 09:51
  • Yes, in fact it is a table in which I have one person per row. In this row, I have an edit button which should call the modal window. Am I right to proceed like that? Also, in your example, if I've understood well, I should create 2 partial views? I've put what I have. – Traffy Apr 15 '13 at 09:54
  • Yes see in my answer in the foreach it goes data-id="@person.PersonId" - this insures that the modal opens with data for the correct person. Have one partial that holds modal head, body, footer and put the code from the last section (modal definition and JS) of the answer in the page where you have your table. If you already list out your models in a foreach, you don't need another one, just implement like I have done in the answer to your own. – MattSull Apr 15 '13 at 09:59
  • Almost done ! My modal appears as it should and I can edit it. However, when I do some changes and push on save, my browser displays an error message which says that `The view 'EditPerson' or its master was not found` . I think we're near to put an end to this. – Traffy Apr 15 '13 at 10:19
  • 1
    Something's probably going wrong in your POST method. Put a breakpoint on it and debug and see what's happening. Actually, in my answer in the POST method I just said return View() for simplicity. You might want to try return PartialView("SomePartial"); – MattSull Apr 15 '13 at 10:23
  • After debugging, I see that it does not enter in the `if(ModelState.IsValid)`. – Traffy Apr 15 '13 at 10:42
  • 1
    You only have two attributes in your view, one of these must not be satisfying validation. Be sure that what you're entering satisfies any validation attributes you've placed on properties in your Person model. Add your person model to the question, or else open a new question specifically for the problem. – MattSull Apr 15 '13 at 10:50
  • Yes it was that... But unfortunately, I have an other error while I want to store my changes. Thanks again, I'll open an other topic. – Traffy Apr 15 '13 at 10:59
  • I don't know if you have any idea on what's going on : http://stackoverflow.com/questions/16013596/mvc-4-optimistic-concurrency-exception But thanks again for all. – Traffy Apr 15 '13 at 11:11
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/28241/discussion-between-traffy-and-mattsull87) – Traffy Apr 15 '13 at 12:00
  • I followed your suggestion but after save, the modal form did not close automatically, why? – Willy Apr 28 '14 at 02:40
  • I used this answer with a web service. That works but anormally slow to get user info. Interesting. – LacOniC Dec 28 '15 at 22:27
  • For the benefit of searchers, this SO answer -http://stackoverflow.com/a/33739233/852806 along with this answer helped me debug this issue. – JsAndDotNet Jan 16 '17 at 15:20
21

I prefer to avoid using Ajax.BeginForm helper and do an Ajax call with JQuery. In my experience it is easier to maintain code written like this. So below are the details:

Models

public class ManagePeopleModel
{
    public List<PersonModel> People { get; set; }
    ... any other properties
}

public class PersonModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    ... any other properties
}

Parent View

This view contains the following things:

  • records of people to iterate through
  • an empty div that will be populated with a modal when a Person needs to be edited
  • some JavaScript handling all ajax calls
@model ManagePeopleModel

<h1>Manage People</h1>

@using(var table = Html.Bootstrap().Begin(new Table()))
{
    foreach(var person in Model.People)
    {
        <tr>
            <td>@person.Id</td>
            <td>@Person.Name</td>
            <td>@person.Age</td>
            <td>@html.Bootstrap().Button().Text("Edit Person").Data(new { @id = person.Id }).Class("btn-trigger-modal")</td>
        </tr>
    }
}

@using (var m = Html.Bootstrap().Begin(new Modal().Id("modal-person")))
{

}

@section Scripts
{
    <script type="text/javascript">
        // Handle "Edit Person" button click.
        // This will make an ajax call, get information for person,
        // put it all in the modal and display it
        $(document).on('click', '.btn-trigger-modal', function(){
            var personId = $(this).data('id');
            $.ajax({
                url: '/[WhateverControllerName]/GetPersonInfo',
                type: 'GET',
                data: { id: personId },
                success: function(data){
                    var m = $('#modal-person');
                    m.find('.modal-content').html(data);
                    m.modal('show');
                }
            });
        });

        // Handle submitting of new information for Person.
        // This will attempt to save new info
        // If save was successful, it will close the Modal and reload page to see updated info
        // Otherwise it will only reload contents of the Modal
        $(document).on('click', '#btn-person-submit', function() {
            var self = $(this);
            $.ajax({
                url: '/[WhateverControllerName]/UpdatePersonInfo',
                type: 'POST',
                data: self.closest('form').serialize(),
                success: function(data) {
                    if(data.success == true) {
                        $('#modal-person').modal('hide');
                        location.reload(false)
                    } else {
                        $('#modal-person').html(data);
                    }
                }
            });
        });
    </script>
}

Partial View

This view contains a modal that will be populated with information about person.

@model PersonModel
@{
    // get modal helper
    var modal = Html.Bootstrap().Misc().GetBuilderFor(new Modal());
}

@modal.Header("Edit Person")
@using (var f = Html.Bootstrap.Begin(new Form()))
{
    using (modal.BeginBody())
    {
        @Html.HiddenFor(x => x.Id)
        @f.ControlGroup().TextBoxFor(x => x.Name)
        @f.ControlGroup().TextBoxFor(x => x.Age)
    }
    using (modal.BeginFooter())
    {
        // if needed, add here @Html.Bootstrap().ValidationSummary()
        @:@Html.Bootstrap().Button().Text("Save").Id("btn-person-submit")
        @Html.Bootstrap().Button().Text("Close").Data(new { dismiss = "modal" })
    }
}

Controller Actions

public ActionResult GetPersonInfo(int id)
{
    var model = db.GetPerson(id); // get your person however you need
    return PartialView("[Partial View Name]", model)
}

public ActionResult UpdatePersonInfo(PersonModel model)
{
    if(ModelState.IsValid)
    {
        db.UpdatePerson(model); // update person however you need
        return Json(new { success = true });
    }
    // else
    return PartialView("[Partial View Name]", model);
}
Dmitry Efimenko
  • 10,973
  • 7
  • 62
  • 79
  • Two Questions: 1. How to Add the validation summary for this example? 2. f.ControlGroup istn available if I copy your code. – float Jul 08 '14 at 12:19
  • I updated the code. (1) You can put `ValidationSummary` anywhere in the code for modal. (2) The `.Bootstrap()` extensions are made available by the use of [TwitterBootstrapMVC](https://www.twitterbootstrapmvc.com/) – Dmitry Efimenko Jul 08 '14 at 16:11
  • this line gives me a ton of errors: @using (var m = Html.Bootstrap().Begin(new Modal().Id("modal-person"))) ::::: there is no Html helper named Bootstrap. Type or namespace "Modal" could not be found. – JustJohn Sep 14 '16 at 17:22
  • @JustJohn, read the comment above that mentions [TwitterBootstrapMVC](https://www.twitterbootstrapmvc.com/) – Dmitry Efimenko Sep 14 '16 at 17:30
  • @Dmitry thank you! My eyes are getting crossed with this modal stuff. – JustJohn Sep 14 '16 at 20:30
3

In reply to Dimitrys answer but using Ajax.BeginForm the following works at least with MVC >5 (4 not tested).

  1. write a model as shown in the other answers,

  2. In the "parent view" you will probably use a table to show the data. Model should be an ienumerable. I assume, the model has an id-property. Howeverm below the template, a placeholder for the modal and corresponding javascript

    <table>
    @foreach (var item in Model)
    {
        <tr> <td id="editor-success-@item.Id"> 
            @Html.Partial("dataRowView", item)
        </td> </tr>
    }
    </table>
    
    <div class="modal fade" id="editor-container" tabindex="-1" 
         role="dialog" aria-labelledby="editor-title">
        <div class="modal-dialog modal-lg" role="document">
            <div class="modal-content" id="editor-content-container"></div>
        </div>
    </div> 
    
    <script type="text/javascript">
        $(function () {
            $('.editor-container').click(function () {
                var url = "/area/controller/MyEditAction";  
                var id = $(this).attr('data-id');  
                $.get(url + '/' + id, function (data) {
                    $('#editor-content-container').html(data);
                    $('#editor-container').modal('show');
                });
            });
        });
    
        function success(data,status,xhr) {
            $('#editor-container').modal('hide');
            $('#editor-content-container').html("");
        }
    
        function failure(xhr,status,error) {
            $('#editor-content-container').html(xhr.responseText);
            $('#editor-container').modal('show');
        }
    </script>
    

note the "editor-success-id" in data table rows.

  1. The dataRowView is a partial containing the presentation of an model's item.

    @model ModelView
    @{
        var item = Model;
    }
    <div class="row">
            // some data 
            <button type="button" class="btn btn-danger editor-container" data-id="@item.Id">Edit</button>
    </div>
    
  2. Write the partial view that is called by clicking on row's button (via JS $('.editor-container').click(function () ... ).

    @model Model
    <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
        <h4 class="modal-title" id="editor-title">Title</h4>
    </div>
    @using (Ajax.BeginForm("MyEditAction", "Controller", FormMethod.Post,
                        new AjaxOptions
                        {
                            InsertionMode = InsertionMode.Replace,
                            HttpMethod = "POST",
                            UpdateTargetId = "editor-success-" + @Model.Id,
                            OnSuccess = "success",
                            OnFailure = "failure",
                        }))
    {
        @Html.ValidationSummary()
        @Html.AntiForgeryToken()
        @Html.HiddenFor(model => model.Id)
        <div class="modal-body">
            <div class="form-horizontal">
                // Models input fields
            </div>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
            <button type="submit" class="btn btn-primary">Save</button>
        </div>
    }
    

This is where magic happens: in AjaxOptions, UpdateTargetId will replace the data row after editing, onfailure and onsuccess will control the modal.

This is, the modal will only close when editing was successful and there have been no errors, otherwise the modal will be displayed after the ajax-posting to display error messages, e.g. the validation summary.

But how to get ajaxform to know if there is an error? This is the controller part, just change response.statuscode as below in step 5:

  1. the corresponding controller action method for the partial edit modal

    [HttpGet]
    public async Task<ActionResult> EditPartData(Guid? id)
    {
        // Find the data row and return the edit form
        Model input = await db.Models.FindAsync(id);
        return PartialView("EditModel", input);
    }
    
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<ActionResult> MyEditAction([Bind(Include =
       "Id,Fields,...")] ModelView input)
    {
        if (TryValidateModel(input))
        {  
            // save changes, return new data row  
            // status code is something in 200-range
            db.Entry(input).State = EntityState.Modified;
            await db.SaveChangesAsync();
            return PartialView("dataRowView", (ModelView)input);
        }
    
        // set the "error status code" that will redisplay the modal
        Response.StatusCode = 400;
        // and return the edit form, that will be displayed as a 
        // modal again - including the modelstate errors!
        return PartialView("EditModel", (Model)input);
    }
    

This way, if an error occurs while editing Model data in a modal window, the error will be displayed in the modal with validationsummary methods of MVC; but if changes were committed successfully, the modified data table will be displayed and the modal window disappears.

Note: you get ajaxoptions working, you need to tell your bundles configuration to bind jquery.unobtrusive-ajax.js (may be installed by NuGet):

        bundles.Add(new ScriptBundle("~/bundles/jqueryajax").Include(
                    "~/Scripts/jquery.unobtrusive-ajax.js"));
Max
  • 531
  • 6
  • 12
  • Thanks for the useful hints. One comment I have is that the server could return a status different from 200 in different situations, for instance due to an internal server error. This will trigger the OnFailure function and replace the content of the modal with whatever is coming back from the server. To solve this, I set a custom header like this `Response.Headers.Add("X-AjaxInvalidModel", "");` then check for its presence on the client before replacing the modal contents: `'isInvalidModel': function (xhr) {return (typeof xhr.getResponseHeader('X-AjaxInvalidModel') === 'string'); }`. – Paul May 14 '17 at 20:53
0

In $('.editor-container').click(function (){}), shouldn't var url = "/area/controller/MyEditAction"; be var url = "/area/controller/EditPartData";?

Andy Narain
  • 55
  • 2
  • 8
  • Is that a question? It looks like one. If it is please delete it and ask it correctly by using the "Ask Question" button. If, to my surprise, you think that this is an answer, then please [edit] to make the answer more obvious and reduce the impression that you are asking your own question in a place where only answers are allowed. – Yunnosch Jul 22 '20 at 19:58