65.9K
CodeProject 正在变化。 阅读更多。
Home

Asp.Net、MVC & jQuery 实现拖放式角色管理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (13投票s)

2014年5月19日

CPOL

8分钟阅读

viewsIcon

48315

介绍如何在 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 行] 创建一个新字符串数组,其中包含从 GrantRoles formItems 集合(请参阅前端部分了解其生成方式)传入的列表的角色。然后将此列表附加到一个隐藏的 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 日 - 首次发布。

© . All rights reserved.