如何构建一个左右选择框
维护多对多表的最佳对话框
引言
多对多关系是指一个人可以属于多个组,一个组也可以包含多个人。在数据库模式中,你不能在person
表中有一个外键指向group
表,因为它只能指向一个行,而实际需求是person
可以属于多个组。所以你需要添加一个名为PersonGroup
的表,其中包含一个由PersonId
和GroupId
组成的多部分键。这是数据库设计中一个相当常见的基本概念。然而,当我寻找用于此功能的图形化对话框工具时,却几乎找不到。
背景
也许我需要一个更好的名称来称呼它。我称之为“左右选择框”,但我可能遗漏了一个常见的描述。正如横幅图片所示,一个选择框由两列列表项和一列带有四个箭头图标的中间列组成。从上到下,按钮分别代表“添加一个”、“添加全部”、“移除一个”和“移除全部”。“添加和移除全部”选项在某些情况下可能适用,但通常很危险,应该禁用。所需功能包括加载列表框的方法,以及添加和移除多对多表中的条目,在本例中是UserRoles
。
代码
这是完整的HTML。在一个容器内有三行:divChooseAvailable
、divChooseButttons
和divChooseAssigned
。其余部分由JavaScript处理。这里使用的例子是将角色分配给用户。数据库处理这是特殊情况。我将在另一篇文章中展示,此处受影响的数据表是AspUserRoles
,它仅对ASP OAuth Owin身份验证系统是唯一的,在本例中是实际的授权系统。由于cookie或其他原因,以编程方式操作这些表非常困难。你可以在SQL Management Studio中操作AspUserRoles
表,但它们除了guid键之外几乎没有其他内容。正确操作roles
表的方法是,在名为Startup
的特殊类中设置一个ApplicationRoleManager
类,并使用OwinStartup
属性进行装饰。我编写了一个特殊的控制器类,它公开了RoleManager
方法。我将很快添加指向该文章的链接。
<div class="crudContainer AssignRolesContainer">
<div class="crudContainerTitle">Assign Roles</div>
<div class="flexContainer">
<div><select class="crudDropDown" id="ddUsers"></select></div>
</div>
<div class="chooseBoxContainer" id="divRolesChoose">
<div class="chooseBox floatLeft" id="divChooseAvailable">
<div class="chooseBtnsContainer floatLeft" id="divChooseButtons">
<img class="chooseBtn"
id="imgAdd" src="Images/IntelDsgn/sglright.jpg" />
<img class="chooseBtn disenabled"
id="imgAddAll" src="Images/IntelDsgn/dblright.jpg" />
<img class="chooseBtn"
id="imgRemove" src="/Images/IntelDsgn/sglleft.jpg" />
<img class="chooseBtn disenabled"
id="imgRemoveAll" src="/Images/IntelDsgn/dblleft.jpg" />
</div>
<div class="chooseBox floatLeft" id="divChooseAssigned"</div>
</div>
</div>
从概念上讲,当一个列表中的项目被选中移到另一个列表时,会在数据库层面执行添加或移除操作,在显示层面,一个列表会减少一个项目,该项目会被添加到另一个列表中。简单吧?我会展示如何做到这一点,但首先需要做一些其他事情。
对话框的初始状态显示未选择用户,所有角色都在左侧列表框中,即“可用项”列表。在进行任何添加或移除操作之前,必须先选择一个User
。你会注意到,在此对话框的顶部有一个<select>
下拉菜单,用于选择多对多关系的枢轴元素。下拉菜单的onChange()
事件会在全局作用域(在本页的js脚本块中)的变量中记住选定的用户。一个警告消息会强制要求你在尝试添加角色之前先选择一个用户。
在我们查看一些功能之前,请允许我问你一个问题。你会把这个onchange
事件的代码放在哪里?这似乎是一个不起眼的细节,但它揭示了JavaScript编程的一个重要原则,当然,你们所有比我聪明的人都已经知道了,但值得一提的是。你会把列表框中项目的onclick
事件放在哪里?
编译器和解释器之间的区别
当你在Visual Studio中按F6“构建”代码时,OBJ文件夹和BIN文件夹中的所有文件都会被创建。通常,只有自上次编译以来被修改过的文件才会被重新构建,但你可以完全删除这些文件夹,然后在下次构建时它们会被完全重建。有时,这样做是件好事。这就是为什么生成菜单上有一个Clean
选项。编译器会逐行检查你的所有代码,并在需要时拉入所需的库和引用,以确保一切正常工作。你可以将代码的各个部分放在任何你想要的位置,编译器会尝试找到它们并确保一切正常。解释器的工作方式不同。
解释器一次读取一行代码,执行它,然后移动到下一行。显然,这简单得多。JavaScript等脚本语言是解释型代码。Visual Studio只能编译你的C#服务器端代码。这就是为什么像Angular这样的框架都着重于在基础模块中设置包含和引用,以克服客户端脚本的逐行、自顶向下的行为。理解解释器的工作方式很重要,即使只是为了更好地了解在哪里放置东西。
我问题的答案是,你必须将事件处理程序(如onchange
或onclick
)的代码放在列表项创建之后。如果你有一个Ajax调用返回列表的元素,并且它有一个成功或回调函数,首先填充你的列表,或者在select
标签的情况下,你在循环中将列表项作为option标签追加。只有这样,你才能编写事件处理程序代码。你期望响应事件处理程序的项目必须首先存在,然后才能告诉它们附加事件。正如我的曾叔叔Olaf常说的那样:先掠夺,然后再烧毁。顺序很重要。如果你已经烧毁了一切,你怎么能掠夺呢?请注意,在onSuccess
回调中嵌入了onClick
代码。
function loadAllItems() {
try{
// $('#divLoadingGif').show();
$.ajax({
type: "GET",
dataType: "Json",
url: "/Admin/GetAllRoles",
success: function (result) {
$('#divChooseAvailable').html("");
$.each(result, function (idx, obj) {
$('#divChooseAvailable').append
("div class="availableListBoxItem chooseBoxListItem"
id=""+ obj.Id + "">"+obj.Name+"/div>;);
});
if (!isNullorUndefined(selectedUserId)) {
getRolesAssignedToUser(selectedUserId)
}
$('.availableListBoxItem').click(function () {
$('.chooBoxListItem').removeClass("highlightItem");
if (isNullorUndefined(selectedName)) {
$(this).addClass("highlightItem");
selectedRoleType = "Available"
selectedRoleName = $(this).text();
}
else {
if (selectedRoleName === $(this).text();) {
selectedRoleId = "" }
else {
$(this).addClass("highlightItem");
selectedRoleType = "Available");
selectedRoleName = $(this).text();
}
}
});
},
error: function (jqXHR, exception) {
alert("error: " + getXHRErrorDetails(jqXHR, exception));
},
});
} catch (e) {
//displayStatusMessage("error", "catch ERROR: " +e);
alert("catch: " + e);
}
}
加载左侧“可用”列表的所有项目的函数和加载右侧列表中“已分配”项目的函数是两个独立的进程。在调试和重构之后,根据我对正确工作流程顺序重要性的讨论,最可靠的解决方案是将这两个函数合并,并确保一个函数在另一个函数之后发生。请注意,除了click
事件外,在加载所有可用项目的函数onSuccess
回调中,还嵌入了调用下一个函数,该函数加载分配给所选用户的可用角色到另一个列中。
当你从第一个数据库回来时,第一件事就是遍历结果集并将角色名称追加到Available
列表中。这基本上是将它们引入存在,创建元素。现在可以实例化onClick()
函数了。我接下来调用load
available函数,因为它执行的Ajax调用是异步的,所以它会直接继续,但它只需要百万分之一微秒,这在计算机时间中足够长,可以使可用列表项稳定并完成任何可能的收尾工作。
视觉界面
在可以添加或移除一个项目之前,还有一件事必须发生,那就是它必须被选中。选择此选项是UI的全部目的。左-右选择框不是处理多对多关系的唯一界面。级联下拉框是另一种流行的解决方案。我不得不说,我记得在1995年HTML推出select标签之前(我们曾经手工构建它们)。ASP组合框控件引入了多选功能。但没有什么比选择框更能直观地表示多对多过程了。我搜索了网络,你找不到更好的实现了。
当用户点击一个项目时,必须提供一个视觉线索。click
事件应用于类;类.avaliableListBoxItem
的所有项目。我喜欢为(大多数)仅用于样式设置的类以及用于编程的类使用不同的类名。$(this
)标签允许我们指定类的单个项目,而无需在构建时知道点击的是哪一个。.addClass()
会给它添加高亮样式。请注意,函数的第一行会从整个类中移除高亮类,因此之前选中的任何项目都会被取消高亮。this
标签只会高亮一个。
还有一件事。处理多对多表时的一个有趣挑战是,你通常没有唯一的主键来处理。AspUserRoles
表仅包含两列:UserId
和RoleId
。因此,你必须处理复合键。这没问题,但Microsoft OWIN处理它的方式是依赖RoleName
,所以我确信这是正确的方法。AspNetRoles
表中的Name
列可能有一个唯一约束。本文不讨论OWIN或授权角色,只是指出管理多对多表可能会带来这个特定挑战。
功能行为
当列表项被点击时,会测试三种状态。如果我们刚开始或者没有选择任何项,isNullorUndefined()
选项会设置选定的项。isNullorUndefined()
在我的global.js文件中,它的功能显而易见。SelectedRoleType
用于防止在选择了右侧列中的已分配项时进行添加,或者在选择了左侧可用列中的项时防止删除。这些特殊情况的bug捕获功能通常比工作代码占用更多的代码和冗长的解释。
第二个if
是一个特殊情况,即当前已选中的项被再次点击。由于类级别的removeClass
命令已经将所有内容恢复为默认样式,所以实际上不需要做更多的事情,但这种视觉提示使界面更加直观,因为这似乎是预期的行为。最后一个情况是默认情况。它将点击的列表项设置为选中并高亮显示。
$(".chooseBtn").click(function () {
if (isNullorUndefined(selectedUserId)) {
displayStatusMessage("warning", "please select a user");
}
else {
if (isNullorUndefined(selectedRoleName)) {
alert("Please select a Role.");
}
else {
switch ($(this).attr("Id")) {
case "imgAdd":
if (selectedRoleType == "Available")
addUserRole(selectedUserId, selectedRoleName);
else
displayStatusMessage("warning", "no available item selected")
break;
case "imgAddAll": // disabled
//$('#divChooseAssigned').html("");
//$('#divChooseAssigned').append($('#divChooseAvailable').html());
//$('#divChooseAvailable').html("");
//perfom special role function
break;
case "imgRemove":
if (selectedRoleType == "Assigned")
RemoveUserRole(selectedUserId, selectedRoleName);
else
displayStatusMessage("warning", "no assigned item selected")
break;
case "imgRemoveAll": // disabled
//$('#divChooseAvailable').append($('#divChooseAssigned').html());
//$('#divChooseAssigned').html("");
//perfom special role function
break;
default:
alert("$(this).Id :" + $(this).attr("Id"))
}
}
}
});
这里,我们有一个case
语句来处理中间列的按钮。本文没有展示一个完整即插即用插件。它是一个可以定制的设计模式示例。数据库调用将特定于实现。但是,它并不是一个特别大或复杂的表单。