I have an Ajax form nested within each row in a table to provide an add / remove feature. The partial lists all roles available (Microsoft identity 2.0), and for each, whether the specified user is associated with that role or not, as well as a button to toggle the user in and out of the role (Ajax).
Everything works fine when I use Ajax.Beginform, however when I use regular jquery, it works the first time I click the button to toggle the user's association with a role, but the second time it loads the partial view on a new page on its own without css styling.
Searching around on stackoverflow, I see others with similar issues, but they seem to not have ajax work at all, unlike me who has it working on the first request, then not subsequently. I should also mention, I am fairly new to Asp.net and this is the first time I am using jquery directly (following Scott Allan's tutorials on PluralSight).
Here's what I've tried already: Checked that the action is being hit by the ajax request with a debug breakpoint. Checked that I am referencing jquery in my layout view, and not more than once. Checked that I am referencing jquery and jquery unobtrusive ajax in the correct order. Checked that web.config has the relevant flags set to true.
I've been stuck on this for hours now, any guidance would be greatly appreciated.
BundleConfig.cs:
namespace GCCP
{
public class BundleConfig
{
// For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.validate*",
"~/Scripts/jquery.unobtrusive*"));
bundles.Add(new ScriptBundle("~/bundles/gccp").Include(
"~/Scripts/gccp.js"));
// Use the development version of Modernizr to develop with and learn from. Then, when you're
// ready for production, use the build tool at http://modernizr.com to pick only the tests you need.
bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
"~/Scripts/modernizr-*"));
bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
"~/Scripts/bootstrap.js",
"~/Scripts/respond.js"));
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/bootstrap.css",
"~/Content/site.css"));
// Set EnableOptimizations to false for debugging. For more information,
// visit http://go.microsoft.com/fwlink/?LinkId=301862
BundleTable.EnableOptimizations = true;
}
}
}
Action methods:
public ActionResult UserRoles(string returnUrl, string UserName)
{
ViewBag.ReturnUrl = returnUrl;
UserRolesViewModel Model = new UserRolesViewModel();
Model.UserName = UserName;
Model.RoleAssignments = UserRoleAssignments(UserName);
return View(Model);
}
[HttpPost]
public ActionResult ToggleUserToRoleAssignment(string returnUrl, string UserName, string RoleName)
{
ViewBag.ReturnUrl = returnUrl;
var userStore = new UserStore<ApplicationUser>(context);
var userManager = new UserManager<ApplicationUser>(userStore);
var roleStore = new RoleStore<IdentityRole>(context);
var roleManager = new RoleManager<IdentityRole>(roleStore);
string userID = userManager.FindByName(UserName).Id;
if ( roleManager.RoleExists(RoleName) == true )
{
if ( userManager.IsInRole(userID, RoleName) == true )
{
userManager.RemoveFromRole(userID, RoleName);
}
else
{
userManager.AddToRole(userID, RoleName);
}
}
ViewModel:
namespace GCCP.Models
{
public class UserRolesViewModel
{
[Required]
[Display(Name = "User")]
public string UserName { get; set; }
public Dictionary<string, bool> RoleAssignments { get; set; }
public string AjaxTest { get; set; }
}
}
View:
@using GCCP.Models;
@using System.Web.Mvc.Ajax;
@model UserRolesViewModel
@{
ViewBag.Title = "User Roles";
}
<h2>
@Model.UserName | Assigned Roles
</h2>
@Html.Partial("_UserRolesList", Model)
Partial View:
@using GCCP.Models;
@using System.Web.Mvc.Ajax;
@model UserRolesViewModel
<div id="roleAssignmentList">
<table class="table table-condensed">
<thead>
<tr>
<th>
Roles
</th>
<th>
Assigned
</th>
<th>
Add / Remove
</th>
</tr>
</thead>
<tbody>
@foreach ( var item in Model.RoleAssignments )
{
if ( item.Value )
{
<tr class="active">
<td>
@item.Key
</td>
<td>
<i class="glyphicon glyphicon-ok"></i>
</td>
<td>
<form action="@Url.Action("ToggleUserToRoleAssignment")" method="post" data-gccp-ajax="true" data-gccp-target="#roleAssignmentList">
<input type="hidden" name="RoleName" value="@item.Key"/>
<input type="hidden" name="UserName" value="@Model.UserName"/>
<input type="submit" value="Change"/>
</form>
</td>
</tr>
}
else
{
<tr>
<td>
@item.Key
</td>
<td>
<i class="glyphicon glyphicon-remove"></i>
</td>
<td>
<form action="@Url.Action("ToggleUserToRoleAssignment")" method="post" data-gccp-ajax="true" data-gccp-target="#roleAssignmentList">
<input type="hidden" name="RoleName" value="@item.Key"/>
<input type="hidden" name="UserName" value="@Model.UserName"/>
<input type="submit" value="Change"/>
</form>
</td>
</tr>
}
}
</tbody>
</table>
</div>
Jquery:
$(function () {
var ajaxFormSubmit = function () {
var $form = $(this);
var options = {
url: $form.attr("action"),
type: $form.attr("method"),
data: $form.serialize()
};
$.ajax(options).done(function (data) {
var $target = $($form.attr("data-gccp-target"));
$target.replaceWith(data);
});
return false;
};
$("form[data-gccp-ajax='true']").submit(ajaxFormSubmit);
});
Thanks in advance :)
"works first time only" behaviour is typical of a situation where the content is being added dynamically, but the events were bound directly to elements (e.g. at DOM ready).
The relevant part would be this:
Which only binds to existing forms present when that line of code was run.
The alternative is to use a delegated event handler, attached to a non-changing ancestor of the elements:
e.g.
It works by listening for the submit event to bubble up to that ancestor, then applies a jquery selector to the elements in the bubble-chain, then applies the frunction.
If you do not have a handy non-changing ancestor use
document
. I am not 100% sure you are not completely replacing#roleAssignmentList
(or just its children), so that example fails usedocument
instead.e.g.
but never use 'body' as a delegated event handler target (it has some bugs relating to styling that means it can miss certain events).