Asp.Net、MVC & jQuery 实现拖放式角色管理
介绍如何在 Asp.Net MVC 项目中实现拖放式角色管理。
引言
早在 2012 年,我就完成了我的计算机荣誉学士学位的毕业项目。该项目是一个 KPI 管理解决方案,旨在将现有的、依赖于多个电子表格和报告的企业工作流程迁移,并开发一种基于 Web 的方法来简化该流程。
在项目开发过程中,出现的一个需求是管理单个用户的权限,例如,谁可以添加/编辑/查看不同的数据集、不同的报告以及管理系统的用户。
我想做一些不同的事情,而不是仅仅提供普通的权限列表,将它们从一个列表视图移动到另一个列表视图,或者使用一个复选框矩阵来开启或关闭用户的权限。
经过一番思考,我考虑使用一种拖放式的 jQuery 方法。这对当时的我来说是新的,并且能够展示我自主学习和解决问题的能力。
在本文中,我将展示该项目中使用的方法。在这篇文章中充斥着所有与项目其余部分无关的内容是没有意义的,所以我们将纯粹关注文章中讨论的用例所需的那些部分。
下面的动画截图可以看到最终的用户管理员角色管理页面;
背景
我使用的是 Asp.Net MVC 3 和 Razor 视图引擎。所有数据都存储在 SQL Express 后端,并使用 EntityFramework V4.3.1 和“Code First”的数据库方法。
已采用标准的 Membership/Roles 提供程序来管理系统中的用户。
这是我第一次使用 MVC、EntityFramework,并且是我第一次真正投入到 C# 的学习中。
正如你可以想象的,为毕业设计项目引入一堆“新东西”是一场巨大的冒险。然而,有几个月的时间 Google 是我的好朋友,并且我非常依赖两个关键的学习信息来源;
后端
角色管理是通过一个控制器 **UserAdminController** 来实现的。有两种主要的操作来管理用户角色;
- GET 操作,返回被管理用户当前拥有的角色列表。
- POST 操作,提供被管理用户现在应该拥有的角色列表。
让我们先来看看完整的 GET 操作;
//
// GET: /UserAdmin/AssignRoles
[Authorize(Roles = "Admin-User-Edit")]
public ActionResult AssignRoles(String username)
{
//Check user is not editing their own roles
if (User.Identity.Name.Equals(username))
{
ModelState.AddModelError("", "You cannot edit your own roles.");
}
else
{
MembershipUser user = Membership.GetUser(username);
if (user == null)
{
ModelState.AddModelError("", "Username is not valid.");
}
else
{
ViewBag.Username = username;
ViewBag.AllRoles = Roles.GetAllRoles();
ViewBag.AllowRoles = Roles.GetRolesForUser(username);
}
}
return View();
}
你首先会注意到,发起此请求的用户必须拥有“Admin-User-Edit”角色[第 4 行],并且被修改用户的用户名只是作为一个字符串传入[第 5 行]。
代码随后会检查发起请求的用户是否试图修改自己的权限[第 8 行]。如果他们试图编辑自己的权限,则会将一条错误消息添加到 ModelState
[第 10 行],然后用户将被返回到视图[第 28 行]。
接下来,我们获取给定用户名的用户对象[第 14 行],如果系统中找不到具有提供的用户名的用户,GetUser
方法将返回 null
,我们会对其进行测试[第 16 行],如果用户名无效,则会将一条错误消息添加到 ModelState
[第 18 行],并且用户将再次返回到视图[第 28 行]。
如果用户名有效,并且我们已获取了相关的 MembershipUser
对象,我们将把管理用户所需的必要数据放入 ViewBag 容器中。
添加了 Username
属性,其中仅包含用户名字符串[第 22 行]。添加了 AllRoles
属性,其中包含系统中所有使用的角色的字符串列表,这是通过调用 GetAllRoles
方法获得的[第 23 行]。最后,AllowRoles
是被修改的用户当前已分配角色的列表。这是通过调用 GetRolesForUser
并将用户名(我们已验证其有效性)传递进去获得的[第 24 行]。
我们现在拥有允许修改用户所需的所有信息,因此我们将相关的视图[第 28 行]传回,稍后我们将对其进行查看。
一旦用户在网页上修改了他们的角色,管理员将使用 POST 事件提交数据。
下面的操作方法是我们控制器中处理此请求的方法;
//
// POST: /UserAdmin/AssignRoles
[Authorize(Roles = "Admin-User-Edit")]
[HttpPost]
public ActionResult AssignRoles(String username, FormCollection formItems)
{
//Check user is not editing their own roles
if (User.Identity.Name.Equals(username))
{
ModelState.AddModelError("", "You cannot edit your own roles.");
}
else
{
try
{
MembershipUser user = Membership.GetUser(username);
if (user == null)
{
throw new ArgumentException();
}
//Update the Roles for the user
String[] newRoles = formItems["GrantRoles"].Split(',');
//Get the list of old roles and remove them
String[] oldRoles = Roles.GetRolesForUser(username);
if (!(oldRoles == null))
{
foreach (String role in oldRoles)
{
if (Roles.RoleExists(role)) { Roles.RemoveUserFromRole(username, role);}
}
}
//Check each new role is valid and apply to user
foreach (String Role in newRoles)
{
if (!(Role.Equals("")) && Roles.RoleExists(Role))
{
Roles.AddUserToRole(username,Role);
}
}
ViewBag.Username = username;
ViewBag.AllRoles = Roles.GetAllRoles();
ViewBag.AllowRoles = Roles.GetRolesForUser(username);
ModelState.AddModelError("","User roles have been updated.");
}
catch (Exception)
{
ModelState.AddModelError("", "Username is not valid");
}
}
return View();
}
POST 操作中有更多内容,让我们逐一分解;
[第 4 行] 我们检查用户是否拥有执行该方法的正确权限。
[第 6 行] 传入被编辑的用户名和网页上的表单元素集合。
[第 10 行] 检查用户是否试图修改自己的角色。
[第 18 行] 获取我们要编辑的用户对象,如果无效则抛出错误[第 22 行]。
[第 26 行] 创建一个新字符串数组,其中包含从 GrantRolesformItems
集合(请参阅前端部分了解其生成方式)传入的列表的角色。然后将此列表附加到一个隐藏的 GrantRoles
元素,之后提交表单。[第 29-36 行] 获取用户当前拥有的角色列表,并从用户中移除每个角色。
[第 38-45 行] 将前面创建的数组中的每个新角色分配给用户。
[第 47-49 行] 使用更新后的信息重建 ViewBag。
[第 51 行] 我们使用 ModelState
添加一条消息,告知用户已更新。
[第 57 行] 之前抛出的任何异常都会通过 ModelState
被简单地处理为一条无效用户消息。
[第 62 行] 返回视图。
此方法确保即使他们手动修改网页属性后再提交,也能保证:
- 只有有权限修改用户的用户才能这样做。
- 用户不能修改自己的权限。
- 只有有效的角色会被添加到用户。
前端
所有视图都共享一个通用布局,其中包含指向 CSS 文件和库文件的引用。在此项目中,我们使用了 jQuery 和 jQueryUI,如下面的布局模板片段所示;
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<link href="@Url.Content("~/Content/themes/base/jquery.ui.all.css")" rel="Stylesheet" type="text/css"/>
<script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery-ui-1.8.18.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/modernizr-2.5.3.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/MicrosoftAjax.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/MicrosoftMvcAjax.js")" type="text/javascript"></script>
<script type="text/javascript">
// Google Analytics Tracking resides here;
</script>
</head>
<body>
// Common Page section elements such as menu headers / footers etc.
</body>
</html>
AssignRoles 页面的视图结构如下;
@{
ViewBag.Title = "Assign Roles";
}
<h2>Assign Roles for User: @ViewBag.Username</h2>
添加了页面标题和一个头部,告知您正在尝试修改哪个用户名。如果您还记得,我们在后端将其添加到 ViewBag
中。
接下来,我们为本页面分配相关的样式。这些是用于两种角色类型(已分配和已拒绝)的无序列表和列表项元素。这些样式允许红色/绿色无序列表具有最小高度,但如果添加的项目多于当前高度,它们将增长。您可以在文章开头的动画截图中看到这一点。
<style type="text/css">
ul.listRoles
{
width: 300px;
height: auto;
padding: 5px;
margin: 5px;
list-style-type: none;
border-radius: 5px;
min-height: 500px;
}
ul.listRoles li
{
padding: 5px;
margin: 10px;
background-color: #ffff99;
cursor: pointer;
border: 1px solid Black;
border-radius: 5px;
}
</style>
接下来是一些用于管理两个列表的 JavaScript。
<script type="text/javascript">
$(function () {
$("#listDenyRoles, #listAllowRoles").sortable({
connectWith: ".listRoles"
}).disableSelection();
});
function submitNewRoles() {
//Generate List of new allow roles
var outputList = $("#listAllowRoles li").map(function () { return $(this).html(); }).get().join(',');
$("#GrantRoles").val(outputList);
$("#formAssignRoles").submit();
}
</script>
第一段代码会自动运行,并设置两个角色列表之间的拖放功能。它们通过 connectWith
建立关联,并在元素 listRoles
中分配相同的类名。这些元素也被设置为可排序,但没有选择功能。
第二部分是 submitNewRoles
函数。它作为表单提交(使用伪提交按钮)和实际表单提交(通过代码触发)之间的中间层。
此函数通过映射 Allow Roles 无序列表中的列表项元素,生成一个由逗号分隔的角色字符串列表。它将此列表附加到一个隐藏的 GrantRoles
元素,然后提交表单。
有关 jQuery.map() 的更多信息,请访问 https://api.jqueryjs.cn/jquery.map/
下一部分为用户提供了一些基本说明,然后创建将使用 HTML.BeginForm
提交回的表单,并将表单的方法设置为 FormMethod.POST
。代码还设置了两个数组,其中包含可用角色列表和用户当前已分配的角色列表。如果 ViewBag 中的任何列表为空,则会创建一个空的字符串列表。
<p>To GRANT a user a role, click and drag a role from the left Red box to the right Green box.<br />
To DENY a user a role, click and drag a role from the right Green box to the left Red box. </p>
@using (Html.BeginForm("AssignRoles", "UserAdmin", FormMethod.Post, new { id = "formAssignRoles" }))
{
String[] AllRoles = ViewBag.AllRoles;
String[] AllowRoles = ViewBag.AllowRoles;
if (AllRoles == null) { AllRoles = new String[] { }; }
if (AllowRoles == null) { AllowRoles = new String[] { }; }
错误消息的 HTML 助手会附加到页面,并添加两个隐藏元素,其中包含用户名和要授予的角色,以便回发。
@Html.ValidationSummary(true)
<fieldset><legend>Drag and Drop Roles as required;</legend>
@Html.Hidden("Username", "Username")
@Html.Hidden("GrantRoles", "")
创建一个表,其中包含两列,一列是拒绝角色,另一列是允许角色。无序列表被添加到相应的列中,并使用样式属性添加背景颜色。红色表示拒绝,绿色表示允许。
两个 foreach
循环会遍历相关的角色列表,并向无序列表中添加一个列表项,该列表项的文本就是角色名。
<table>
<tr>
<th style="text-align: center">
Deny Roles
</th>
<th style="text-align: center">
Allow Roles
</th>
</tr>
<tr>
<td style="vertical-align: top">
<ul id="listDenyRoles" class="listRoles" style="background-color: #cc0000;">
@foreach (String role in AllRoles)
{
if (!AllowRoles.Contains(role))
{
<li>@role</li>
}
}
</ul>
</td>
<td style="vertical-align: top">
<ul id="listAllowRoles" class="listRoles" style="background-color: #00cc00;">
@foreach (String hasRole in AllowRoles)
{
<li>@hasRole</li>
}
</ul>
</td>
</tr>
</table>
最后,我们添加一个按钮,它充当提交按钮并触发 submitNewRoles
函数。
<p><input type="button" onClick="submitNewRoles()" value="Assign Roles"/></p>
</fieldset>
}
就是这样!看起来相对简单,但我为此付出了不少努力。尤其是使用 jQuery 将列表元素映射回字符串列表。
值得注意的是,可能存在更好、更优化的方法来实现这一点,但对于我必须处理的小范围角色来说,这对我来说是可接受的。
例如,在分配新角色时,上述方法会简单地移除用户的所有角色,然后应用新的角色列表。有人可能会争辩说,为什么要移除一个角色,而可能又要把它加回来。我觉得简单地从用户的角色列表开始,既更清晰,风险也更小。这样你就不会留下一个本不该存在的角色。
演示项目
很遗憾,由于这是来自一个现有项目,我没有时间从头构建一个演示。然而,文章的叙述中有足够的信息,您可以轻松地自己完成类似的工作。
历史
2014 年 5 月 19 日 - 首次发布。