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

如何构建一个左右选择框

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (5投票s)

2018年12月24日

CPOL

9分钟阅读

viewsIcon

11056

维护多对多表的最佳对话框

引言

多对多关系是指一个人可以属于多个组,一个组也可以包含多个人。在数据库模式中,你不能在person表中有一个外键指向group表,因为它只能指向一个行,而实际需求是person可以属于多个组。所以你需要添加一个名为PersonGroup的表,其中包含一个由PersonIdGroupId组成的多部分键。这是数据库设计中一个相当常见的基本概念。然而,当我寻找用于此功能的图形化对话框工具时,却几乎找不到。

背景

也许我需要一个更好的名称来称呼它。我称之为“左右选择框”,但我可能遗漏了一个常见的描述。正如横幅图片所示,一个选择框由两列列表项和一列带有四个箭头图标的中间列组成。从上到下,按钮分别代表“添加一个”、“添加全部”、“移除一个”和“移除全部”。“添加和移除全部”选项在某些情况下可能适用,但通常很危险,应该禁用。所需功能包括加载列表框的方法,以及添加和移除多对多表中的条目,在本例中是UserRoles

代码

这是完整的HTML。在一个容器内有三行:divChooseAvailabledivChooseButttonsdivChooseAssigned。其余部分由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这样的框架都着重于在基础模块中设置包含和引用,以克服客户端脚本的逐行、自顶向下的行为。理解解释器的工作方式很重要,即使只是为了更好地了解在哪里放置东西。

我问题的答案是,你必须将事件处理程序(如onchangeonclick)的代码放在列表项创建之后。如果你有一个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()函数了。我接下来调用loadavailable函数,因为它执行的Ajax调用是异步的,所以它会直接继续,但它只需要百万分之一微秒,这在计算机时间中足够长,可以使可用列表项稳定并完成任何可能的收尾工作。

视觉界面

在可以添加或移除一个项目之前,还有一件事必须发生,那就是它必须被选中。选择此选项是UI的全部目的。左-右选择框不是处理多对多关系的唯一界面。级联下拉框是另一种流行的解决方案。我不得不说,我记得在1995年HTML推出select标签之前(我们曾经手工构建它们)。ASP组合框控件引入了多选功能。但没有什么比选择框更能直观地表示多对多过程了。我搜索了网络,你找不到更好的实现了。

当用户点击一个项目时,必须提供一个视觉线索。click事件应用于类;类.avaliableListBoxItem的所有项目。我喜欢为(大多数)仅用于样式设置的类以及用于编程的类使用不同的类名。$(this)标签允许我们指定类的单个项目,而无需在构建时知道点击的是哪一个。.addClass()会给它添加高亮样式。请注意,函数的第一行会从整个类中移除高亮类,因此之前选中的任何项目都会被取消高亮。this标签只会高亮一个。

还有一件事。处理多对多表时的一个有趣挑战是,你通常没有唯一的主键来处理。AspUserRoles表仅包含两列:UserIdRoleId。因此,你必须处理复合键。这没问题,但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语句来处理中间列的按钮。本文没有展示一个完整即插即用插件。它是一个可以定制的设计模式示例。数据库调用将特定于实现。但是,它并不是一个特别大或复杂的表单。

如何构建一个左右选择框 - CodeProject - 代码之家
© . All rights reserved.