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

使用 ASP.NET MVC 创建桌面用户体验(第 2 部分,共 3 部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2017 年 3 月 24 日

CPOL

33分钟阅读

viewsIcon

12849

downloadIcon

849

构建 ASP.NET MVC 桌面用户体验(User Experience)。查看上下文菜单的实际效果。

引言

如果您阅读了本系列的第一部分(使用 ASP.NET MVC 创建桌面用户体验(第 1 部分,共 2 部分)[^])或者您了解构成 ASP.NET MVC 基础的概念,那么您就可以开始构建我们在本文中将要构建的应用程序的核心功能了。

在本系列文章的第二部分中,我们将着手创建提供桌面用户体验的应用程序。

final target context menu

在撰写本文时,我确定我们需要增加一个章节(第 3 部分),因为在完成本部分(第 2 部分)的内容后存在一个自然的过渡。这是因为本部分完全专注于 UI / UX。

本文涵盖内容

本文的重点是完成整个 UI / UX 工作。

完成整个 UI / UX

在完成整个 UI / UX 的过程中,我们将

  • 通过构建代码并附带每个步骤的代码下载来逐步进行
  • 了解添加到我们 UX 中的每一行代码的目的
  • 仔细研究 CSS 样式,使用许多样式来创建我们的 Windows 窗体概念和上下文菜单。
  • 使用 JavaScript 构建提供用户体验的操作
  • 在 Visual Studio 中查看 Nuget 的实际应用,检索 jQueryUI 并了解其工作细节
  • 了解如何利用 jQuery 功能以及 jQuery 选择器如何使用等内容。
  • 通过将 Bootstrap 类应用于我们的 HTML 来实现一些 Bootstrap 样式

阅读完本文后,您将拥有

  1. 一个可重复使用的完整可拖动窗口窗体元素
  2. 右键单击窗体元素时出现的风格化的上下文菜单
  3. 一个允许用户选择其他功能运行的上下文菜单
  4. 一个可工作的上下文菜单的框架

第 3 部分:实现最终功能

然后在第 3 部分中,我们将实现最后一个功能,该功能将

  • 实现 MVC 控制器和 JavaScript Ajax 调用,以处理来自上下文菜单的异步请求
  • 实现部分视图,将结果数据显示给用户,并显示在结果视图中,该视图也表示为一个 Windows 窗体元素。

现在,让我们开始构建带有可工作上下文菜单的 Windows 窗体元素。

Windows 窗体元素

为了创建桌面用户体验,我在这款应用程序中首先想要的是 Windows 窗体元素的概念。这个元素将是我们 UI 的一部分,我们使用 HTML 和 CSS 来构建基于 Web 的 UI。

设置窗体元素的样式

我将使用一个简单的 `<div>` 标签和一些将添加到 Site.css 中的样式来创建这个元素。当添加样式时,我会解释我们使用的样式,并用屏幕截图展示 UI 的进度。

创建并添加背景颜色

我们将采取的第一步是

  1.  添加一个 `<div>`
  2. 设置背景颜色(以便您可以看到 div 的位置)
  3. 为 `<div>` 设置边框
  4. 为 `<div>` 设置高度和宽度

要完成这些项目,我将向我们项目的主要样式表文件 site.css 添加一些样式。

CSS 样式是名称/值对

我不会过多地讨论 CSS 样式,但您应该知道它们只是名称/值对,其中名称由 CSS 标准预定义。 

所有名称-值对都包含在 `{ }` 括号内,名称与值用冒号 `:` 分隔,每个名称/值对用分号 `;` 与其他对分隔。

当然,您还需要为样式命名,以便以后可以引用它。名称可以是类(由点 . 表示)、ID(由 `#` 表示)或预定义 HTML 标签的名称(`<body>`、`<li>`、`<div>` 等),这样样式就会应用于网页上所有该类型的标签。

一个通用的 CSS 样式看起来像

.className{

    name1:value1;

   name2:value2;

}

#idName{

    name1:value1;

   name2:value2;


}

body{

    name1:value1;

   name2:value2;
}

空格无关紧要

当然,空格无关紧要,因为 CSS 使用标记(分号、冒号、括号)来分隔它查找的所有项目。这意味着上一个示例中的第一个可以写成如下形式,并且仍然完全有效。

.className{ name1:value1;name2:value2; }

将样式应用于 HTML 元素

前面的示例显示了如何在 .CSS 文件中定义样式。但是,它们直到应用于 HTML 后才会显示。

当然,要将样式应用于特定的 HTML 元素,您必须将其添加到 HTML 中。

将类样式应用于 HTML

例如,如果您想在 HTML 中的 `<div>` 上应用之前定义的 CSS 类样式,您可以在 `<div>` 标签中添加一个 class 属性,并添加定义的类样式的名称。它看起来像以下内容

<div class="className">this block would be styled as defined in className CSS defintion</div>

将 ID 样式应用于 HTML

如果您想应用您使用 `#` 来指示它是 ID 元素的样式,您将设置 `<div>` 标签上的 `id` 属性。

<div id="idName">this block would be styled as defined in idName CSS defintion</div>

引用样式时不要使用特殊符号

请注意,**我们只在样式表中定义类或 ID 时使用特殊符号**。在 HTML 中引用其名称时,我们不使用符号。这是因为为了指示其类型,我们使用属性名称 `id` 或 `class`。

应用于预定义 HTML 标签

对于为 `<body>` 标签(或其他任何预定义标签)定义的样式,只要该样式表在该页面上加载,该样式就会应用于页面上找到的整个元素。这使您可以定义将始终应用于某个标签的样式,而不仅仅是在由 `id` 或 `class` 样式指示时。

只需了解名称属性和可能的值

现在您知道了 CSS 的结构,您只需要知道有效的名称属性(如 background-color、padding、border-style)然后了解指定名称属性的可能值。一旦您知道了这些(可以在网上轻松查找),您就可以轻松地为您的 HTML 设置样式。

例如,如果您想使用 CSS 设置某个项目的背景颜色,您需要知道该属性的 CSS 名称是:background-color。之后,您只需要知道该属性有效的值和值类型。

我刚用谷歌搜索了:background-color css,第一个结果是:CSS background-color 属性[^],并看到了类似如下的代码示例

body {
    background-color: yellow;
}

h1 {
    background-color: #00ff00;
}

p {
    background-color: rgb(255,0,255);
}

正如您所见,有多种格式可用于设置背景颜色。

对 CSS 的理解就差不多了,我们可以开始创建我们的样式了。

可拖动类样式

我创建了一个类样式,我将其命名为 draggable,因为最终我希望我的 div 允许用户单击它并在浏览器中拖动它。到目前为止,我定义它的方式如下。

.draggable {
            background-color:mintcream; 
            border-color:black;
            border-style:solid;
            overflow:hidden;
            border-radius: 25px; 
            padding: 0.5em; 
            cursor: move;
 }

将此添加到您的 `site.css` 文件并保存。

其中大部分样式都是不言自明的,您可以猜测它们的作用。看起来我添加了一个黑色边框,并将背景颜色设置为预定义颜色。我之所以这样做,是为了让您能够分辨 `<div>` 在网页上的实际位置。其他元素,如 padding(在 div 的内边缘和任何文本之间留出一些空间)和 border-radius(给边框边界边缘添加曲线),则不太明显,但您可以查阅更多详细信息。最后一个属性很有趣,因为我决定将光标更改为移动光标(四向箭头),以表明您可以抓取 **`<div>`**。

Site.css 在哪里加载?

让我们回到我们非常熟悉的 _Layout.cshtml,在上篇文章中我们已经熟悉了它,看看 site.css 文件是如何链接的。这就是 site.css 文件加载的方式,以便应用其样式。

在 `_Layout.cshtml` 的顶部,我们看到以下内容

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" type="text/css" />
    <script src="~/Scripts/modernizr-2.6.2.js"></script>
</head>

从服务器获取,加载到浏览器内存中

当然,加粗的行是我们加载 site.css 的行。当浏览器看到这一行时,它会从服务器获取文件并将样式加载到内存中,以便应用它们。

Bootstrap 奖励

您还可以看到,在链接 site.css 的那一行之后,布局模板还加载了 Bootstrap 样式。Bootstrap 本质上就是这样:一堆预定义的样式,为用户创建通用的外观和感觉。我们将使用其中一些,因为它们可以使我们轻松地获得一些否则会花费大量工作的样式。

让我们应用我们的样式并查看

我们正在处理的目标视图是 Index.cshtml,所以请打开该文件。

我将添加一个带有我们新样式的 div 和几行假数据,以便我们可以在页面上看到 div。

这是我们目前为止 Index.cshtml 的完整列表。

@{
   
    ViewBag.Title = "Index";
}
<div class="draggable">
    <p>data</p>
    <p>more data</p>
    <p>extra data</p>
    <p>other data</p>
    <p>various data</p>
</div>
<h2 >Index</h2>

当然,我们这里关注的是我添加的新 `<div>` 标签。您可以看到我用我们新的 `draggable` 类对其进行了样式设置。

添加新的 HTML 后,请再次运行应用程序。**注意**:我还从我们的 `ContextMenu.js` 中删除了 `alert` 对话框,这样我就不必每次都点击 [OK] 了。

first run with new styled div

获取源代码下载

如果您下载本文顶部的代码 v002(v001 在第一篇文章中提供),您可以运行应用程序并从这一点继续。

现在我们已经完成了所有这些基础知识,并且您对我们进行这些操作(如定义样式)的地方有了很好的理解,我们可以在开发中加快速度。

使 Div 可拖动

我们刚刚定义的 `<div>` 是我将要专注于创建 Windows 窗体式用户体验的元素,这正是本系列文章的全部目的。我现在想让该元素可拖动,就像 Windows 桌面上的 Windows 窗体一样。这将开始创建我所期望的用户体验。

但是,当您研究如何使 HTML 元素可拖动时,您会发现一些声称有效但实际上并未完全实现解决方案的答案。我发现最简单的方法是,它可以在所有主要浏览器中工作。

不仅仅是样式,回到 JavaScript

要使拖动功能正常工作,我们实际上必须在 JavaScript 中实现它。如果您仔细想想,就需要有东西来确定 `<div>` 的 x,y 坐标位置,然后在移动它时更新它,所以它可能比我们最初认为的要复杂。

但是,我发现了一个最简单的使 div 可拖动的方法,同时只包含一个小型库。这个附加库是 jQuery 的姐妹库。它是 jQueryUI。使用 Nuget 在 Visual Studio 中也很容易获取。

我们这样做。

通过 Nuget 获取 jQueryUI

什么是 Nuget?

Nuget 是 Visual Studio 从 2010 年及更高版本开始内置的包管理器。什么是包管理器?它是一个有用的第三方实用程序,可以为我们安装通用的代码库。

Nuget 安装基础 MVC 库

Nuget 安装所有基础 MVC 库,所以它已经在您的项目中工作了。当您从本文获取源代码并首次运行时,也是该实用程序恢复包。

**注意**:Nuget 包管理器可能会很麻烦。如果您不想执行这些步骤,只需**下载本文顶部的 V003 版本项目**。它已经引用了所需的 jQueryUI 库。

在 Visual Studio 中,在顶部菜单栏找到 `TOOLS` 菜单项并单击它。

向下移动到 Nuget Package Manager 菜单项,然后移到 Manage Nuget Packages for Solution...

nuget pkg manager

一个窗口将出现,现在让我们快速查看一下 Nuget 已经管理的全部包。

单击对话框左侧的 `[Installed Packages]` 项,它看起来会像以下内容

install pkgs

现在,单击左侧的 `[Online]` 项(位于 `[Installed Packages]` 项下方),在该选项下应该有一个 `[Microsoft and .NET]` 项。选择它。

当您选择 [Microsoft and .NET] 时,UI 会刷新并显示不同的库。我们要找的是 jQuery UI(Combined Library)。您可以在右上角的搜索框中输入 jqueryui(一个单词),它应该会显示我们需要的确切库。

jqueryUI package

在右侧,您可以看到一些版本信息和有关该库的详细信息。

jqueryUI details

单击 `[Install]` 按钮,将弹出一个确认对话框。

confirm pkg man install

单击 [OK],包和依赖项将被安装。

jqueryUI installing

完成后,安装对话框将消失,Nuget 包管理器仍会显示在屏幕上。单击右下角的 [Close] 按钮,您将返回到 Visual Studio。

Nuget 包管理器和 Studio 问题

Nuget 有一些问题。它在 Visual Studio 中使用仍然不完美。

存在一些问题

  1. Visual Studio 2013 中的 ASP.NET MVC 模板引用了一个旧版本的 jQuery(1.10.2)。
  2. 在 _Layout.cshtml 中,我们加载了原始 jQuery 版本(~/Scripts/jquery-1.10.2.min.js)。
  3. 但是,当我们选择 jQueryUI 库并将其添加到我们的项目中时,Nuget 包安装程序会将 jQuery 更新到它需要的一个新版本。这样做会删除 v1.10.2。但是,它不会更新我们的 _Layout.cshtml 文件以引用新版本。这意味着 jQuery 根本不会加载,因为它引用了一个不存在的旧文件。

这是原始 Scripts 文件夹的视图,之前(在 Nuget 安装之前)和之后(右侧)。

old scripts foldernew scripts folder

您可以看到 1.10.2 jQuery 已消失。但是,实际上还有一个问题,因为 Nuget 没有清理 jquery-1.10.2.intellisense.js(一个 Visual Studio 编辑器辅助版本,用于在 JS 中显示智能感知)。

回到 _Layout.cshtml 更新脚本引用。

编辑 _Layout.cshtml

打开 _Layout.cshtml,滚动到文件底部,删除旧的脚本引用 `jquery-1.10.2.min.js`。

添加两个新引用

<script src="~/Scripts/jquery-1.12.4.min.js"></script>
<script src="~/Scripts/jquery-ui-1.12.1.min.js"></script>

确保将这些引用放在 bootstrap.js 和 ContextMenu.js 之前,因为这些脚本将实际引用这两个新脚本。

现在我们终于可以实现一些代码来使 `<div>` 可拖动了。

使用 jQuery 选择器使类可拖动

jQueryUI 提供了一个非常好的方法,允许我们使任何元素可拖动并处理所有工作。它可能不 100% 完美,但对于其实现的简易性来说非常出色。我相信它也会说服您,值得添加额外的库来获得此功能。

只需一行代码!

现在,我们只需要在我们 ContextMenu.js 文件中添加一行代码。我们将把它添加到页面加载后调用的函数中。

这是 ContextMenu.js 中所有代码,添加的行已加粗。

//ContextMenu.js

$(function() {
        $(".draggable").draggable();
    }
);

jQuery 选择器

让我们分解这行简短的代码,以便我们知道它的作用。 

它做了两件事

  1. 选择目标元素
  2. 向该元素应用功能(draggable)

jQuery 选择器允许我们仅通过包含其类名或 ID 名称来选择任何元素。

jQuery 选择器的形式为: 

$(".className") $("#idName") 

它们允许我们选择一个项(DOM 元素),以便我们可以对其进行操作。

jQuery 选择器确实要求我们在访问类或 ID 时包含点 `.` 或 `#` 的特殊字符。因此,该行的第一部分通过查找具有 `class="draggable"` 的那个来查找或选择我们的 div 元素。我的命名使一切看起来有点多余,但尽管如此,jQuery 还是找到了我们的 `div`,然后对其运行了一个名为 `draggable()` 的方法。那个 `draggable()` 方法使该项…嗯…可拖动。 :)

休斯顿,我们有拖放

重新构建并运行,现在您只需单击 div 中的任何位置即可拖动它并放下。这真的很酷。 

这里有一个动画 GIF,您可以看看。

drag and drop in action

当然,我们还有很多工作要做才能使其有用。

另外,您在视频中注意到当我移动 `<div>` 时,它会滑到顶部的导航栏下方?这与这些项目上的 CSS 属性 `z-index` 有关,我们稍后会修复它。

获取代码下载 V003

您可以通过下载本文顶部的 v003 版本来获取迄今为止所做的所有工作。

还远远不够

当然,成功不会持续太久,因为现在我可以在任何地方抓住 `<div>` 来移动它,而 Windows 窗体并不像这样工作。相反,您只能抓住顶部的菜单栏来移动 Windows 窗体。我有一个想法该怎么做,所以让我们更改一些样式,看看我是否能做到。

实际上,首先,让我们更改 Index.cshtml 中 `<div>` 元素的结构,因为它不太对。

添加另一个 Div

在 `Index.cshtml` 中,让我们按如下方式更改 div

@{
    ViewBag.Title = "Index";
 }
<div class="mainForm">
    <div class="titleBar">Title Bar</div>
    <p>data</p>
    <p>more data</p>
    <p>extra data</p>
    <p>other data</p>
    <p>various data</p>
</div>
<h2 >Index</h2>

我已将 `<div>` 类的名称更改为 `mainForm`,并向其中添加了一个新的嵌套 `<div>`。**注意**:请注意,我现在在名称上使用了驼峰式命名法。

新的嵌套 `<div>` 将代表我们的窗体标题栏,它将是窗体中可抓取(或可拖动)的部分,所以我现在将其命名为 `titleBar`。这是一个更具描述性的名称,更容易在 CSS 中找到。我们将不得不更改样式,以便它们引用正确的 `<div>`,但这很容易。

现在让我们打开 `site.css` 并进行样式更改。有几个样式我很喜欢,并且很兴奋地向您展示。

修改样式

MainForm 样式

首先,让我们看看 mainform 类样式。

.mainform{background-color:mintcream; 
          border-color:black;
          border-style:solid;
          border-radius: 25px;
          z-index: 100000;
}

外层 div 具有黑色圆角矩形边框(border-radius),就像之前一样。它仍然具有薄荷绿背景颜色。那里没有太大变化,只是现在我们不需要在外层 div 上显示移动光标。相反,我们希望它仅在用户将鼠标悬停在标题栏上时出现。

我还添加了 `z-index` 值并将其设置为十万(值越高,元素越靠上),以确保我们的 `<div>` 位于所有其他元素之上。稍后,在动画 GIF 中,我们将看到这如何使我们的 `<div>` 浮动到顶部的导航栏之上。

标题栏是事情变得有趣的地方。

TitleBar 样式

起初我只有以下三个样式,但正如您将看到的,效果不太好。

.titleBar {
    background-color: lightblue;
    overflow:hidden;
    padding: .5em; 
    cursor: move;
}

这就是它现在的渲染效果。还不太对。 :)

first shot titlebar issue

正如您所见,标题栏的上角没有像预期的那样圆角,无法与外层 `mainForm` `<div>` 匹配,而且看起来不好。我用谷歌搜索并找到了另一个 CSS 样式,我很满意。

这是完整的 titleBar CSS,其中两个新样式已加粗。

.titleBar {
    background-color: lightblue;
    border-top-right-radius: 25px; 
    border-top-left-radius: 25px;
    overflow:hidden;
    padding: .5em; 
    cursor: move;
}

现在看起来像

rounded title bar

这样看起来更好。但是,您仍然无法抓住标题栏。这需要一些 JavaScript 工作。让我们现在就来做。我花了一些时间才弄清楚这一点,因为我了解到 jQueryUI .draggable() 方法必须先初始化,否则它就无法正常工作。一旦我弄明白了,就相当直接了。

JavaScript,让生活更轻松

JavaScript,让生活充满乐趣。 :) 我知道很多人不这么认为。但是,明智地使用 JavaScript 可以成为您的朋友。

首先,让我们看一下 `ContextMenu.js` 的完整代码列表。这使得只有 `titleBar` 可以被抓取,但整个外部 `<div>` 在您拖动时会移动。 

//ContextMenu.js
var isDraggable = false;
$(function () {
        // these first calls to draggable() initialize the jQuery method for use
        $(".mainForm").draggable();
        $(".mainForm").draggable("disable");
        $(".titleBar").mousedown(mouseDownToggleDrag);
        $(".titleBar").mouseup(mouseUpToggleDrag);
    }
);

function mouseDownToggleDrag() {
    console.log("mousedown");
    isDraggable = true;
    toggleDrag();
}

function mouseUpToggleDrag() {
    console.log("mouse up");
    isDraggable = false;
    toggleDrag();
}

function toggleDrag() {
    if (isDraggable) {
        console.log("turn on...");
        $(".mainForm").draggable("enable");
    } else {
        console.log("turn off...");
        $(".mainForm").draggable("disable");
    }
    console.log("isDraggable : " + isDraggable.toString());
}

我在其中放置了一些注释,并且还保留了我的一些 `console.log()` 语句,以便您可以跟随并查看代码是否运行。

页面加载时的初始化

页面加载时,我们进行一些初始化。从注释可以看出,我吸取了痛苦的教训,知道需要调用 `draggable() `方法在我的元素上以初始化该函数。在此之前,该方法只会拖动 `<div>` 一次,之后每次都会失败。

注册 MouseDown 和 MouseUp 事件

接下来的两行使用 jQuery 在 `titleBar` 元素上注册 `mousedown` 和 `mouseup` 事件。这使我们能够知道这些事件何时发生。在这里,我们提供了一个要调用的函数,以便在这些事件发生时我们可以做一些工作。

MouseDown 事件

当鼠标按下时,我们做两件事

  1. 将全局变量(`isDraggable`)设置为 true
  2. 调用 `toggleDrag()` 方法

MouseUp 事件

mouseup 事件执行相同的两件事,只是它将 `isDraggable` 设置为 false。 

这两个事件共同确保当用户单击标题栏时,整个 `<div>` 是可拖动的,而当她单击 `<div>` 内的任何其他位置时,则不可拖动。

我们离桌面用户体验(User Experience)的目标又近了一步。这里有另一个关于可拖动 `<div>` 的动画 GIF。

drag mvc in action 2

下载源代码 v004

此时,您可以下载本文附带的 v004 源代码,并保持同步。

现在我们有了一个提供初步桌面用户体验的基础 UI,我们可以开始实现我所设想的功能了。 

本文系列还有更多内容

起初我以为我可以在两篇文章中写完,但为了将内容分解成更容易消化的小块,我将把这个系列延长到第三篇文章,涵盖应用程序的核心功能。 

核心功能将利用 ASP.NET MVC 框架和 Razor 视图来检索和显示数据。但是,我们将构建的 ContextMenu 将更多地使用 JavaScript,而这正是我们目前正在进行的工作。所以对于本文,让我们构建 ContextMenu 并使其显示,然后在第三篇文章中,我将向您展示如何使用它来检索数据。

构建上下文菜单

ContextMenu 功能概述

我正在驱动的核心功能是

  1. 浏览器桌面中的一个类似窗体的对象,用于显示数据。
  2. 用户可以右键单击数据行以显示一个上下文菜单,该菜单提供用户可以选择的选项,以获取有关该数据行的其他信息。
  3. 当用户在上下文菜单中做出选择时,会发出 AJAX 调用到服务器。
  4. 服务器查找相关数据并响应一个 MVC PartialView,该视图将信息显示给用户。

ContextMenu HTML 元素 

我们需要做的第一件事是创建一个 HTML 元素来表示我们的上下文菜单。在这里,我们再次将使用极其灵活的 `<div>` 元素作为我们的基本结构。让我们从以下代码开始。

<div class="RADContextMenu">

</div>​​​

我创建了一个新的 HTML / CSS 类 `RADContextMenu`,我们将用它来设置我们的上下文菜单的样式,并从 JavaScript 引用该元素。

进入 Bootstrap

我已经尝试过 Bootstrap 样式足够多,我知道我想应用 Bootstrap 面板样式。

您可以在官方 Bootstrap 网站上看到更多内容(Bootstrap 面板[^])。随着我们的进展,Bootstrap 预定义的样式将帮助我们比自己编写所有 CSS 更快地达到外观和感觉,并且 Bootstrap 默认包含在 MVC 项目中。

<div class="RADContextMenu panel panel-primary">
        <div class="panel-body">

        </div>
 </div>

这些额外的类属性(panel、panel-primary 和 panel-body)都由 Bootstrap 定义,并将应用于这些 `<div>` 元素。

我们应该在哪里添加新的 Div?

当我们向这个 `<div>` 添加更多样式时,它只会在用户在 mainForm 元素上右键单击后显示。这意味着这个 `<div>` 可以放在 `_Layout.cshtml` 文件中的任何位置。我将其放在文件底部,在所有 `<script>` 标签之前。这是文件底部区域的快照,以便您可以看到我添加它的位置。 

new div location

如果您构建并运行,您实际上会看到 `<div>`,但我们会创建一个样式使其在用户右键单击之前隐藏。它位于下一个屏幕截图的最底部:带有蓝色边界的区域。

panel is visible

让我们添加新的 CSS,它将在我们需要它之前隐藏此面板。我们还将尝试复制 Windows 窗体上下文菜单样式的样式。

创建上下文菜单样式

打开您的 site.css 并添加以下新样式

.RADContextMenu{
         z-index:10000000;
         visibility:hidden;
         display:none;
         width:200px;
         height:150px;
         position:absolute;
         font-family:'Microsoft Sans Serif';
         font-size:11px;
      }

您可以看到我们将 `visibility` 设置为 `hidden`,因此该项目将不会显示。我们还设置了 `display` 属性为 `none`。这确保了即使 `<div>` 被隐藏,它也不会占用 DOM 中的任何空间。如果您不添加此属性,您将看到一个空白区域,即 `<div>` 所在的位置。

宽度和高度:菜单项数量

目前,我们设置了宽度和高度,以便当该项目显示时,它会占用一定的空间。稍后我们将不得不更详细地讨论这一点,因为它取决于有多少菜单项会出现在上下文菜单上。

Position:显示在用户右键单击的位置

我们还设置了 `position` 为 `absolute`,这意味着无论其他元素是什么,它都会在浏览器内的确切 x, y 坐标处放置。这是因为我们将上下文菜单精确定位在用户右键单击的任何位置的右侧。但是,要完成这项工作,我们必须编写一些 JavaScript。

最后,我们设置 `font` 和 `font-size` 来模仿 Windows 窗体上下文菜单。

当然,如果您构建并运行,您将不再看到上下文菜单元素,因为我们已经将其隐藏了。

让我们编写 JavaScript 并将其添加到 ContextMenu.js 中,以便在用户右键单击时显示它。

Z-Index 属性再次出现

这个元素也有一个 z-index,这次我将其设置为 10,000,000。这很荒谬,但它只是为了确保它始终位于任何其他元素之上。还记得我们设置 mainForm 的 z-index 为 1,000,000 吗?我们可以稍后为 z-index 找到更好的值。现在,这样就可以了。

使用 JavaScript 显示上下文菜单

跟踪鼠标移动

我们需要做的第一件事是跟踪鼠标移动。这是因为我们需要知道右键单击时的 X、Y 位置,以便将上下文菜单绘制在光标位置,就像在 Windows 窗体上一样。即使使用 jQuery,注册任何浏览器也很容易。 

$(document).mousemove(onMouseMove);

就是这样。现在,每当鼠标在 Web 浏览器中的文档上移动时,`onMouseMove()` 函数就会被调用。我们的 `onMouseMove()` 函数将仅获取当前的 x, y 位置并将其存储在全局变量 `mousePos` 中供我们使用。

function onMouseMove(e) {
    mousePos = { x: e.clientX, y: e.clientY };
}

在 JavaScript 文件顶部定义全局 mousePos 变量,并使用括号表示法 { } 将其初始化为对象。

var mousePos = {};

每次移动鼠标时,mousePos 的 x 属性和 y 属性都会更新。然后我们可以稍后访问该值。

测试 MouseMove

如果您想测试鼠标移动,可以在 onMouseMove() 函数的最后一行添加以下行,然后构建并运行。然后,当您在 Index.cshtml 上移动鼠标时,请检查您的控制台,您将看到值正在变化。

console.log("mousePos.x : " +mousePos.x.toString() + "  mousePos.y : " + mousePos.y.toString());

它会产生很多输出,所以请在完成后将其删除或注释掉。

现在我们知道鼠标的 x, y 位置,我们可以确保在那个位置绘制我们的上下文菜单。 

知道何时显示浏览器上下文菜单

现在事情变得有趣了。我们想显示自定义上下文菜单,但是,浏览器本身提供了上下文菜单。我们需要覆盖它,但同时也要允许在我们需要它的时候显示它。

这是 Chrome 中渲染的默认上下文菜单。

default context menu

首先,我们只想在用户右键单击我们的 mainForm 元素时显示客户上下文菜单。我们可以使用 jQuery 来注册该事件。实际上有一个 jQuery `on()` 方法,它允许您提供您想了解的事件。在我们的情况下,我们想要 `on("contextmenu")`,以便当默认上下文菜单将要显示时,我们将其中断。当我们中断它时,我们希望它运行我们的自定义代码。 

Shift 键显示默认上下文菜单

但是,如果用户按住 Shift 键,我将说我们忽略自定义代码并向他们显示默认上下文菜单。

preventDefault() 覆盖浏览器事件

当用户不按住 Shift 键时,我们的上下文菜单将出现,并且它将阻止默认菜单显示。我们可以通过调用浏览器事件模型提供的 `e.preventDefault()` 方法来实现这一点。 

这是注册此 contextmenu 事件的完整代码列表。

$(".mainForm").on('contextmenu', function (e) {
                if (e.shiftKey === false) {
                    drawContextMenu();
                    e.preventDefault();
                }
            });
        }

显示我们的上下文菜单的自定义代码位于 `drawContextMenu()` 方法中。

function drawContextMenu() {
    isContextMenuDisplayed = true;
    $(".RADContextMenu").css({ 'visibility': 'visible', 'display': 'block' });
    $(".RADContextMenu").css({ 'top': mousePos.y, 'left': mousePos.x });
}

一旦知道该怎么做,一切就都很简单了。代码量很少,这让我很高兴。特别是 JavaScript。 :) 

drawContextMenu() 如何工作

drawContextMenu() 将全局变量 `isContextMenuDisplayed` 设置为 true,以便稍后我们可以确定上下文菜单当前是否在屏幕上。在某些情况下,当它显示时,我们可能想隐藏菜单。例如,当用户显示上下文菜单然后不想运行任何功能时,她可以单击菜单外部的某个位置,它就会消失。 

接下来,我们使用 jQuery 选择器获取对我们菜单的引用。当我们这样做时,我们调用一个名为 `css()` 的 jQuery 辅助函数,它允许我们以编程方式为元素设置一些样式。如您所见,我们正在将 visibility 设置为 visible,并将 display 属性设置为 block,以便它能够正确地呈现在其他元素之上。

之后,我们使用 `css()` 方法设置 top 属性,该属性是我们目标 `<div>` 的左上角的 x, y 坐标。这就是我们使用 mousePos 对象的地方,以便我们可以将上下文菜单绘制在光标位置。

就这样。现在我们可以再次构建并运行,我们将看到上下文菜单。

blank context menu

我点击了那个位置,上下文菜单现在显示了。您可以看到上下文菜单是空白的,因为我们没有添加任何单独的项目,但至少它现在显示了。让我们暂时添加一些假项目,以便我们可以看到它会是什么样子。我们通过添加使用 HTML `<ul>`(无序列表)和 `<li>` 列表标签的列表来添加项目。 

添加上下文菜单项

回到 `_Layout.cshtml` 并滚动到 `RADContextMenu <div>`,让我们添加以下代码(在下一个代码列表中加粗)。

<div class="RADContextMenu panel panel-primary">
  <div class="panel-body">
    <ul>
      <li onclick="onContextMenuClick(this)" id="lookupValue" class="contextMenuItem">Get Lookup Value</li>
      <li onclick="onContextMenuClick(this)" id="randomInteger" class="contextMenuItem">Get Random Integer</li>
      <li onclick="onContextMenuClick(this)" id="randomWord" class="contextMenuItem">Get Random Word</li>
      <li onclick="onContextMenuClick(this)" id="randomFloat" class="contextMenuItem">Get Random Float</li>
      <hr class="hrContextMenu" />
      <li onclick="onContextMenuClick(this)" id="lastError" class="contextMenuItem">Get Last Error</li>
      <hr class="hrContextMenu" />
      <li onclick="onContextMenuClick(this)" id="nothing" class="contextMenuItem">Do Nothing</li>
     </ul>
    </div>
</div>

我们可以添加该代码,再次构建并运行,但事情仍然不太对。 

menu step 2

我们需要添加更多样式来帮助我们。 

鼠标悬停在菜单项上时,项目应更改

在真实的上下文菜单中,您悬停的项会改变颜色。 此外,我需要解释点击每个项目时会发生什么。让我们添加样式,查看更改,然后我们将修复更多 JavaScript。

我们实际上需要三个新样式

  1. 出现在 RADContextMenu 上的 `<ul>` 元素的样式
  2. 出现在 RADContextMenu 上的 `<li>` 元素的样式
  3. 出现在 RADContext 菜单上的 panel-body 的样式(修改 Bootstrap 样式)

基本上,我们需要清除列表项上自动出现的那些项目符号(点),并且需要清除项目的一些 padding 和 margin。

我们只需将以下 CSS 添加到 site.css 中即可完成这项工作。

.RADContextMenu ul{
        /*width: 100%;  */
        list-style-type: none; 
        margin: 0px 0px 0px 0px;
        padding: 0px 0px 0px 0px;
          
}

.RADContextMenu li{
          /*width:100%;*/
          margin: 0px 0px 0px 0px;
          padding: 3px 0px 3px 15px;
}

.RADContextMenu .panel-body{
        width: 100%;
        padding: 0px 0px 0px 0px;
        margin: 0px 0px 0px 0px;
}

我还添加了几个 `<hr>` 水平线标签,它们渲染为水平线,它们看起来不太对,所以我还需要修改它们的 CSS。为这些 `<hr>` 标签添加以下内容。

.hrContextMenu{
    display: block; height: 1px;
    border: 0; border-top: 1px solid #4281F4;
    margin: 0 0 0 0; 
    padding: 0 0 0 0;
}

如果您回到 _Layout.cshtml,您可以看到所有这些样式都已应用。

context menu looks good

现在上下文菜单看起来相当不错。而且,我相信如果您一直关注,您就会真正理解我们是如何完成所有事情的。

当然,您无法从静态图片中看出,但是当您将鼠标悬停在每个项目上时,样式会发生变化,并且感觉非常响应用户。

我们只需要另一个样式来实现这一点,因为 CSS 提供了我们可以使用的 hover 属性。让我们在鼠标悬停时将背景设置为蓝色,文本变为白色。

.contextMenuItem:hover {
    background-color: #4281F4;
    color:white;
}

我们的上下文菜单是样式完成的!!

当然,您无法从静态图片中得知,但是当您将鼠标悬停在每个项目上时,样式会发生变化,并且感觉非常响应用户。

context menu style complete

获取源代码:下载 v005

右键单击显示菜单,然后将鼠标悬停在菜单上非常有趣,您必须尝试。

您可以通过下载本文顶部的 V005 来获取到目前为止的代码。现在就做,并尝试一下那个出色的悬停功能。 :)

功能如何触发?

现在,我们只需要挂钩 JavaScript,以便当用户选择一个菜单项时,就会触发特定的功能。如果您回顾我们在 `_Layout.cshtml` 中的 HTML 代码列表,您会看到我们添加了 `onclick()` 处理程序。这是一个在元素被点击时会触发的方法。它看起来像这样

<li onclick="onContextMenuClick(this)" id="lookupValue" class="contextMenuItem">Get Lookup Value</li>

该 HTML 确保无论何时单击该项目,都会运行 `onContextMenuClick()` 方法。我们需要在 `ContextMenu.js` 文件中定义该方法并为其编写实现。

另请注意,该方法将 `this` 对象作为参数。在这种情况下,`this` 对象表示被单击的 DOM 元素。这样,同一个方法就可以处理每个单独的 `<li>` 被单击时的功能。您将在 JavaScript 方法中看到它会检查 `this` 对象 `id` 的值,以便我们知道哪个元素被单击了。 

让我们看一下代码的实现。

onContextMenuClick() 实现

当单击上下文菜单项时,我们需要做的第一件事是隐藏上下文菜单。这是因为一旦用户选择了该项目,菜单就不再是必需的,而且会碍事。

function onContextMenuClick(e)
    {
      console.log("onContextMenuClick()");
      hideContextMenu();
      console.log(e.id);
    }

在这种情况下,我使用 `console.log` 将输出到控制台,以确保菜单单击事件正在触发。在此之后,我调用另一个我们需要编写的方法 `hideContextMenu()`,它将隐藏菜单。

最后,我写出了该方法调用的元素的 ID。这样我们就可以查看值并确保我们的思路是正确的。首先,让我们添加 `hideContextMenu()` 方法,然后尝试运行代码,看看当我们单击菜单项时会得到什么。

hideContextMenu():让菜单再次消失

function hideContextMenu() {
    if (isContextMenuDisplayed) {
        $(".RADContextMenu").css({ 'visibility': 'hidden', 'display': 'none' });
        $(".RADContextMenu").css({ 'visibility': 'hidden', 'display': 'none' });
        isContextMenuDisplayed = false;
    }
}

这段代码实际上是我们想要显示上下文菜单时所做操作的反向,以及 `drawContextMenu()` 中的功能。

现在我们对我们想要发生的事情有了基本结构,让我们再次运行代码,当我们单击菜单项时,观察控制台窗口。

running context menu to console

代码下载 v006

您可以获取源代码并尝试一下,方法是下载本文顶部的 V006 代码。下载后,构建并运行,在浏览器中运行时按 F12 键打开控制台。

之后,开始单击菜单项,ID 值将出现,上下文菜单将隐藏自身。

添加另外两项

好了,只剩下两件事要添加,我们就有了 Windows 窗体元素和上下文菜单的基础。

我们将添加

  1. 当用户单击可见上下文菜单以外的任何位置时,调用 hideContextMenu()。
  2. 一个 switch 语句,它允许我们根据选择了哪个菜单项来运行不同的功能。

让我们完成这两件事,然后我们就可以在第 3 部分中完成我们的应用程序所需的一切了。

在未单击时隐藏上下文菜单

为了解决当上下文菜单可见但未被单击时隐藏它的问题,我们只需要再加一行代码。这行代码为整个文档注册 `click()` 事件。只要文档上发生单击,就会调用 `hideContextMenu`。如果上下文菜单已显示,则会隐藏它。否则,则不会。

$(document).click(hideContextMenu);

我们只需将此添加到 `ContextMenu.js` 开头的 `load` 事件中。

您可以从本文顶部的 v007 版本下载源代码来尝试一下。这将是最终的源代码列表,也将包含我们剩下的工作。

将 Switch 语句添加到 onContextMenuClick()

为了确保在单击特定菜单项时运行自定义代码,我们只需根据 `id` 获取的值进行 `switch`。代码看起来像这样。请注意,我再次使用 console.log 来模拟此代码。在第 3 部分中,我们将最终为这些语句中的每一个发出 AJAX 调用到服务器。

function onContextMenuClick(e) {
    console.log("onContextMenuClick()");
    hideContextMenu();
    console.log(e.id);
    switch (e.id) {
        case "lookupValue":
        {
            alert("I am looking up a value.");
        }
        case "randomInteger":
        {
            alert("I am generating a random integer");
            break;
        }
        case "randomWord":
        {
            alert("I am  generating a random word.");
            break;
        }
        case "randomFloat":
        {
            alert("I am  generating a random float.");
            break;
        }
        case "lastError":
        {
           alert("The last error was : system failure.");
            break;
        }
        case "nothing":
        {
            alert("I am doing nothing.");
            break;
        }
    }
}

我现在已将功能实现为警报对话框。这是它运行时外观的快照。 

final working menu

对于您选择的每个菜单项,您都会收到一个不同的 `alert` 对话框消息。

获取最终下载源代码 V007

从本文顶部获取最终下载(v007)并进行尝试。我们通过这篇文章学到了很多东西,并取得了长足的进步。下次,在第 3 部分,您将看到这如何与异步调用服务器以显示真实数据协同工作。下次见。

历史

2017/03/24 -- 代码和文章首次发布

© . All rights reserved.