Ajax post in MVC 3 with multiple-form View

2019-02-15 13:01发布

问题:

I deleted my prior question dealing with the subject as the context has changed a bit and the problem is new.

I have a single view now, with a single model. The view has 2 forms. Each form submits to its own action, each action uses a subset of the model data, uses the data to perform a search. The search results then are added to a property of the model and need to be rendered back to the view. I'm able to accomplish this all via a standard html post but this now needs to be an ajax post.

Here is my ViewModel:

public sealed class SearchUsersViewModel {

    [Display(Name = "Last Name")]
    public string LastName { get; set; }

    [Display(Name = "Username")]
    public string Username { get; set; }

    [Display(Name = "Email Address")]
    public string EmailAddress { get; set; }

    [Display(Name = "Entity Type")]
    public byte EntityTypeID { get; set; }

    [Display(Name = "Search Name")]
    public string SearchField { get; set; }

    public IEnumerable<SelectListDTO> EntityTypes { get; set; }

    public IEnumerable<UserEditViewModel> Users { get; set; }

    [Display(Name = "Total Rows")]
    public int TotalRowCount { get; internal set; }

    public SearchUsersViewModel() {
        EntityTypes = LookupEntityTypeService.Instance.SelectList;
        Users = new List<UserEditViewModel>();
    }
}

Here is my View:

@model SearchUsersViewModel

<div>
    <div class="SearchByUserDataTable">
        @using (Html.BeginForm("FilterByUserData", "Admin", FormMethod.Post, new {model = Model})) {
            @Html.ValidationSummary(true)
            <table cellpadding="0" cellspacing="0" border="0">
                <tr style="height: 30px;">
                    <td class="Header01">
                        User Search:
                    </td>
                </tr>
            </table>
            <table cellpadding="0" cellspacing="0" border="0">
                <tr style="height: 20px;">
                    <td class="Header02">
                        Search By User Information:
                    </td>
                </tr>
            </table>
            <table cellpadding="0" cellspacing="0" border="0">
                <tr style="height: 1px;">
                    <td class="ContentDividerHoriz_425"></td>
                </tr>
            </table>
            <table id="searchByUserDataTable" cellpadding="0" cellspacing="0" border="0">
                <tr style="height: 26px;">
                    <td class="leftColumn">
                        @Html.LabelFor(model => model.LastName):
                    </td>
                    <td class="rightColumn">
                        @Html.TextBoxFor(model => model.LastName, new { id = "lastNameSearchField", @class = "TextField_220" })
                    </td>
                </tr>
                <tr style="height: 26px;">
                    <td class="leftColumn">
                        @Html.LabelFor(model => model.Username):
                    </td>
                    <td class="rightColumn">
                        @Html.TextBoxFor(model => model.Username, new { id = "userNameSearchField", @class = "TextField_220" })
                    </td>
                </tr>
                <tr style="height: 26px;">
                    <td class="leftColumn">
                        @Html.LabelFor(model => model.EmailAddress):
                    </td>
                    <td class="rightColumn">
                        @Html.TextBoxFor(model => model.EmailAddress, new { id = "emailAddressSearchField", @class = "TextField_220" })
                    </td>
                </tr>
            </table>
            <table cellpadding="0" cellspacing="0" border="0">
                <tr>
                    <td id="filterByUserError" style="width: 375px;"></td>
                    <td align="right" style="width: 50px; padding-right: 75px;">
                        <div>
                            <input id="filterByUserButton" type="submit" value="Search" />
                        </div>
                    </td>
                </tr>
            </table>
        }
    </div>

    <div class="SearchByEntityDataTable">
        @using (Html.BeginForm("FilterByEntityData", "Admin", FormMethod.Post, new { model = Model })) { 
            <table cellpadding="0" cellspacing="0" border="0">
                <tr style="height: 28px;">
                    <td style="width: 425px;"></td>
                </tr>
            </table>
            <table cellpadding="0" cellspacing="0" border="0">
                <tr style="height: 20px;">
                    <td class="Header02">
                        Search By Entity Information:
                    </td>
                </tr>
            </table>
            <table cellpadding="0" cellspacing="0" border="0">
                <tr style="height: 1px;">
                    <td class="ContentDividerHoriz_425"></td>
                </tr>
            </table>
            <table id="searchByEntityDataTable" cellpadding="0" cellspacing="0" border="0">
                <tr style="height: 26px;">
                    <td class="leftColumn">
                        @Html.LabelFor(model => model.EntityTypeID):
                    </td>
                    <td class="rightColumn">
                        @Html.DropDownListFor(model => model.EntityTypeID, new SelectList(Model.EntityTypes, "ID", "Name"), new { id = "entityTypeDropDown", @class = "DropDown_220" })
                    </td>
                </tr>
                <tr style="height: 26px;">
                    <td class="leftColumn">
                        @Html.LabelFor(model => model.SearchField, new { id = "entityTypeSearchLabel"}):
                    </td>
                    <td class="rightColumn">
                        @Html.TextBoxFor(model => model.SearchField, new { id = "entityTypeSearchField", @class = "ui-widget TextField_220" })
                    </td>
                </tr>
                <tr style="height: 26px;">
                    <td class="leftColumn"></td>
                    <td class="rightColumn"></td>
                </tr>
            </table>
            <table cellpadding="0" cellspacing="0" border="0">
                <tr>
                    <td id="filterByEntityError" style="width: 375px;"></td>
                    <td align="right" style="width: 50px; padding-right: 75px;">
                        <div>
                            <input type="submit" value="Search" />
                        </div>
                    </td>
                </tr>
            </table>
        }
    </div>

    <div id="searchResults">
        @(Html.Telerik().Grid(Model.Users)
            .Name("Users").TableHtmlAttributes(new { style = "width: 870px;"})
            .Columns(columns => {
                columns.Bound(o => o.EntityTypeName).Title("Entity Type");
                columns.Bound(o => o.FirstName).Title("First Name");
                columns.Bound(o => o.LastName).Title("Last Name");
                columns.Bound(o => o.Username).Title("Username");
                columns.Template(
                    @<text>
                        <a href="mailto:@item.EmailAddress" target="blank">@item.EmailAddress</a>
                    </text>).Title("Email").HtmlAttributes(new { style = "text-align: center" }).HeaderHtmlAttributes(new { style = "text-align: center" });
                columns.Template(
                    @<text>
                        <div class="ActionIcon_ResendInvitationOn" title="Resend Invitation" onclick="location.href='@Url.Action("ResendInvitation", "Admin", new { entityTypeID = item.EntityTypeID, emailAddress = item.EmailAddress })'"></div> 
                        @{
                            if (item.IsApproved) {
                                <div class="ActionIcon_AccountStatusOn" title="Disable Account" onclick="setApprovalStatus('@item.Username', false);"></div> 
                            }
                            else {
                                <div class="ActionIcon_AccountStatusOn" title="Enable Account" onclick="setApprovalStatus('@item.Username', true);"></div> 
                            }
                        }
                        @{
                            if (item.IsLockedOut) {
                                <div class="ActionIcon_ResetPasswordOn" title="Unlock Account" onclick="unlockAccount('@item.Username')"></div> 
                            }
                            <div class="ActionIcon_ResetPasswordOn" title="Reset Password"  onclick="resetPassword('@item.Username')"></div> 
                        }
                        <div class="ActionIcon_EditOn" title="Edit User" onclick="location.href='@Url.Action("Edit", "Admin", new { id = item.MembershipID, username = item.Username })'"></div>
                    </text>).Title("Actions");
                columns.Bound(o => o.RowNumber).Hidden(true);
                columns.Bound(o => o.MembershipID).Hidden(true);
                columns.Bound(o => o.EntityTypeID).Hidden(true);
            })
            .Pageable(paging => paging.PageSize(10))
            .Sortable()
        )
        <span>Total Rows: </span> @Html.DisplayFor(model => Model.TotalRowCount)

        <p>
            @Html.ActionLink("Create New User", "Create", "Invitation")
        </p>
    </div>
</div>

I have used this view with this line as the BeginForm:

@using (Html.BeginForm("FilterByUserData", "Admin", FormMethod.Post, new {model = Model})) 

And using this:

 @using (Html.BeginForm())

with this Ajax script

$('#filterByUserButton').click(function () {
    $.ajax({
        url: '/Admin/FilterByUserData',
        type: 'POST',
        success: function (result) {
            alert(result);
        }
    });
});

With the first method using the HTML post it works fine but I need to use the ajax functionality so the 2nd method is my trouble

1) Using the ajax call the model being posted to the action contains all Null values in the text fields instead of the values I'm entering on the form. The model posts fine using the HTML method

2) Even if I can get the model to post correctly so that I have values to search with, how do I return the data to the grid and how do I show model errors when the validation fails instead of sending the result to the UpdateTargetID?

3) Being able to have both forms submit to the controller and have the same results back to the view, a search result

It seems I can only have 1 UpdateTarget - which is fine if you never have errors...but the behavior is bad, I need to show the validation errors on the field just the same as if this was a synchronous call. So the error result coming from the action would need to have it's own target.

I came up with this Ajax call that works fine with actions that don't need to return data, just a success or error message. Each gets loaded into its own div.

$(function () {
    $('form').submit(function () {
        if ($(this).valid()) {
            $.ajax({
                url: this.action,
                type: this.method,
                data: $(this).serialize(),
                success: function (result) {
                    if (result.toString().indexOf("Success") == -1) {
                        $('#successDiv').hide();
                        $('#errorDiv').html('');
                        $('#errorDiv').fadeIn(100).append($('<ul />').append(result));
                    }
                    else {
                        $('#errorDiv').hide();
                        $('#successDiv').fadeIn(1000).html(result).fadeOut(6000);
                    }
                }
            });
        }
        return false;
    });
});

These are the actions that process the search (The FilterByUserData action is different as I was trying to work out how to get a proper result back, I don't yet have it returning data properly.

    public ActionResult Search() {
        var model = new SearchUsersViewModel();
        return View(model);
    }

    [HttpPost]
    public ActionResult FilterByUserData(SearchUsersViewModel model) {
        var result = string.Empty;
        if (model.LastName != null || model.Username != null || model.EmailAddress != null) {
            var listOfMatchingUsers = SearchUserService.SearchByUserData(model.LastName, model.Username, model.EmailAddress);
            model = PrepareResultsModel(listOfMatchingUsers, model);
        }
        else {
            ModelState.AddModelError("", "Last Name, Username or Email Address must be entered for search");
        }
        if (ModelState.IsValid)
            result = "Success: Thanks for your submission: " + model.Username;
        else {
            result = ModelState.SelectMany(item => item.Value.Errors).Aggregate(result, (current, error) => current + error.ErrorMessage);
        }
        return Content(result, "text/html");
    }

    [HttpPost]
    public ActionResult FilterByEntityData(SearchUsersViewModel model) {
        if(model.EntityTypeID > 0 &&  model.SearchField != null) {
            var listOfMatchingUsers = SearchUserService.SearchByEntityData(model.EntityTypeID, model.SearchField);
            model = PrepareResultsModel(listOfMatchingUsers, model);
        }
        else {
            var entityType = string.Empty;
            if(model.EntityTypeID == 2) entityType = "Lender";
            if (model.EntityTypeID == 3) entityType = "School";
            ModelState.AddModelError("", entityType + " name must be entered for search");
        }
        return View("Search", model);
    }

    private SearchUsersViewModel PrepareResultsModel(ICollection<SearchUserResultsDTO> listOfMatchingUsers, SearchUsersViewModel model) {
        if (listOfMatchingUsers.Count != 0) {
            model.Users = listOfMatchingUsers.Select(item => new UserEditViewModel(item)).ToList();
            model.TotalRowCount = model.Users.Count();
        }
        return model;
    }

How can I get the model for the ajax post to make it to my action? and how do I get a proper model back so that my grid has the results? and if there is an error how can I get that shown in a similar manner to validationsummary?

Update: for @Shyju I tried what you suggested trying to incorporate your example into my code but the values of the properties of the model are still null and are not being set. This is what I did in my ajax function, note that it is different than your form.post method:

$('#filterByUserButton').click(function () {
    $.ajax({
        url: '/Admin/FilterByUserData',
        type: 'POST',
        data: {LastName : $('#LastName').val(), Username : $('#Username').val(), EmailAddress : $('#EmailAddress').val()},
        success: function (result) {
            processResult(result);
        }
    });
});

I also tried this:

$('form').submit(function() {
    alert("submitting");
    $.post('@Url.Action("FilterByUserData","Admin")', { LastName: $("#LastName").val(), UserName: $("#Username").val(), EmailAddress: $("#EmailAddress").val() },
        function(data) {
            alert(data);
    });
});

The page flashes and the first alert is it but it never hits the 2nd alert and never hits the controller so it looks like it's not even truly posting. I also tried this but it also does not hit the controller action:

$('#filterByUserButton').click(function () {
    alert("submitting");
    $.ajax({
        url: '/Admin/FilterByUserData',
        type: 'POST',
        data: {LastName : $('#LastName').val(), Username : $('#Username').val(), EmailAddress : $('#EmailAddress').val()},
        success: function (result) {
            alert(result);
            processResult(result);
        }
    });
});

I also tried the above with the data: line omitted and it will hit my controller action but again, the model being sent has null values in the properties. The model isn't null, just the property values. I do have values in the dropdown list properties of the model, the EntityTypes still show 4 values. So there's something about posting that form but the form values are not making it into the model properties.

Update 2

@Shyju I'm using your ajax code like so:

$(function () {
    $('#SearchByUserForm').submit(function () {
        $.post('@Url.Action("FilterByUserData", "Admin")', { LastName: $("#LastName").val(), UserName: $("#UserName").val(), EmailAddress: $("#EmailAddress").val() },
        function (data) {
            alert(data);
        });
        return false;
    });
});

I can step thru this after hitting the submit button but it fails to hit the post action of my controller. I looked at the HTML rendered for the form and this is what I see.. which is incorrect:

<form action="/Admin/Search/SearchByUserForm" method="post">         

As you can see the POST action here is wrong. The Controller is correct but the rest is wrong. It should be posting to /Admin/FilterByUserData. I'm sure this is just a syntactical mess that I'm in and I'm just not sure what I'm doing wrong. In a standard form I would do this:

@using (Html.BeginForm("FilterByUserData", "Admin", FormMethod.Post, new { model = Model })) { 

Which works correctly. But I can't combine this form signature with the URL action post from your Ajax code. I tried just leaving the form stark with only the ID but I get the problem I stated above:

@using (Html.BeginForm(new {@id = "SearchByUserForm"})) {

I think this problem comes about from the fact that my view is rendered by hitting the Search action of my controller, so the URL is /Admin/Search but the forms on the view need to post to /Admin/FilterByUserData and /Admin/FilterByEntityData. Like I said above if I explicitly create the Html.BeginFrom with the right signature then it works,, but it's just not Ajax

Update 3

@Shyju

I got it working, the problem was definitely the Form signature, I modified it and I can hit my controller action successfully and also the data is being passed in! Thanks for all your help!

This is what got it working:

@using (Html.BeginForm(new {id = "SearchByUserForm", @controller = "Admin", @action = "FilterByUserData"})) {

回答1:

Use the same name as of your property name (of your viewmodel which is the parameter of your action method) when you pass data in your jquery post. you will get the object filled with data there.

('form').submit(function () {
    $.post('@Url.Action("Logon","Account")', { LastName : $("#username").val(), 
                     UserName:   $("#password").val() },
                     EmailAddress:   $("#password").val() },  function (data) {

    //process the result and update the grid

});

EDIT : Since the OP said, it is not working, I created a sample project from scratch to verify this and it is working fine. Here is my View looks like

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>

<h2>Index</h2>
<form id="frm1" action="Search">
LastName <input type="text" id="LastName" /><br />
UserName <input type="text" id="UserName" /><br />
Email Address <input type="text" id="EmailAddress" />
<input type="submit" value="go" />
</form>
<script type="text/javascript">
    $(function () {
        $('#frm1').submit(function () {
            $.post('@Url.Action("Search", "Home")', { LastName: $("#LastName").val(),
                UserName: $("#UserName").val(),
                EmailAddress: $("#EmailAddress").val()
            }, function (data) {

                alert(data)
            });
            return false;
        });
    });
</script>

And my action method which receives the post call is

public ActionResult Search(SearchUsersViewModel objVM)
{
   return View();
}

And here is the screenshots of results

Client

Controller Action method

You can even send the form to the controller action using the jquery serialize() method as well.

http://api.jquery.com/serialize/