MVC 研究
学习创建动态菜单、内联编辑控件和自定义对话框,包括一个简单的图片选择对话框,允许从图库中选择图片。使用 image-picker 和 bootbox 第三方库。
引言
这是为您的网页部分提供内联编辑功能的一种方法,同时也包含了从服务器上可用的图片中选择图片的能力。
背景
本例的案例研究是一个简单的产品类别列表。每个添加的产品类别都将自动获得一个登陆页面和一个菜单条目。我们的创建和编辑视图也将实现内联编辑控件,并将通过服务器端和客户端脚本的结合,使用户能够从服务器上已有的图片中选择一张。画廊将使用 bootbox 库和 image-picker 库以模态方式呈现给用户。
必备组件
这是我的开发环境的简要概述,作为尝试跟随本指南的一个建议起点。它绝不是唯一可行的环境。
- Microsoft ASP.NET 5 Framework (现称为 ASP.NET Core)
http://www.asp.net/core - Microsoft Visual Studio Community 2015
https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx
安装 VS Community 时,请务必安装 Web 开发包。
第一步 - 创建项目
首先,让我们在“Web”下创建一个新的项目,类型为 ASP.NET Web 应用程序,并命名为 *__ProductsIncorporated__*。
注意: 如果您没有此类模板,则很可能需要安装 Visual Studio 的 Web 开发组件。执行安装修复,并确保选择 Web 开发工具。
接下来,对于 ASP.NET 模板,选择 ASP.NET 5 Web 应用程序。如果此模板不可用,您很可能需要安装 ASP.NET 5 Framework。
开箱即用,此模板已为您的网站提供了一个相当强大的框架。如果您是 MVC 新手,请花点时间看看。运行项目,看看它的行为。如果您想通过一个很棒的 MVC 教程来快速上手,请看这个指南
http://www.asp.net/mvc/overview/getting-started/introduction/getting-started
准备好后,让我们继续实现我们自己的 **模型 (Model)**、**视图 (Views)** 和 **控制器 (Controller)**。
第二步 - 模型
让我们看看解决方案资源管理器。您会看到已经创建了大量的项。其中,有 Models、Views 和 Controllers 目录。
首先,让我们创建我们的模型,我们将其命名为 ProductCategory。右键单击 Models 目录,然后选择 Add -> Class。将新类命名为 ProductCategory.cs。下面是我们的模型类应该是什么样子。
using System;
using System.ComponentModel.DataAnnotations;
namespace ProductsIncorported.Models
{
public class ProductCategory
{
[Key]
public int Id { get; set; }
[Display(Name = "Category Name")][Required]
public String CategoryName { get; set; }
[Required]
public String Description { get; set; }
[Required]
public String CategoryImageUrl { get; set; }
}
}
由于我们将需要进行数据迁移……我们稍后会讲到……所以我想先完成模型,然后逐一解释其中的部分。首先,请注意,我们包含了 System.ComponentModel.DataAnnotations 库的 using 语句。这使我们能够访问一些强大的 DataAnnotation 标签,从长远来看可以为我们节省大量工作。首先,对于我们的 Id 属性,我们使用了 [Key] 注解,它告诉数据库这是我们的主键字段。接下来,我们使用 [Display] 注解来为该字段定义一个用户友好的名称,该名称稍后将在我们的控件中自动使用。最后,[Required] 注解将为我们提供数据验证的快速访问。
第三步 - 控制器
接下来,让我们为 ProductCategory 创建控制器类。我们可以利用 Visual Studio 的脚手架功能来创建控制器,它还会为我们创建相关的视图。在解决方案资源管理器中,右键单击 Controllers 目录,然后选择 Add -> Controller。选择“MVC 6 Controller with views, using Entity Framework”,然后单击 Add。然后将显示以下对话框。
对于 Model Class,让我们选择我们刚刚创建的 ProductCategory 模型,对于 Data Context class,选择唯一的选项 ApplicationDbContext。确保选中 Generate Views、Reference Script Libraries 和 Use Layout Page 选项,并将 Layout page 留空,这将使用默认页面布局方案。单击 Add,Visual Studio 将开始创建所需的脚手架。您会看到项目添加了几个内容:ProductsController 类,以及 Views 中的 Products 子目录,以及管理数据库所需的几个默认视图。您现在可以启动页面,然后浏览到 _https://:port/Products_,但是,您会收到一个错误,因为我们还没有将模型更改迁移到数据库中。
现在我们已经创建了模型、视图和控制器,我们需要使用 Visual Studio 附带的 dnx 工具将其迁移到我们的数据架构中。目前没有为此的工具栏入口,所以我们将不得不使用命令提示符来实现它,但它相当简单。右键单击解决方案资源管理器中的 ProductsIncorporated 目录,然后选择“Open Folder in File Explorer”选项。使用 Windows 资源管理器向上移动一个目录,进入“src”文件夹。按住 Shift 右键单击 ProductsIncorporated 文件夹,然后选择“Open Command Prompt Here”。现在我们可以通过 2 条命令来实现数据迁移,首先创建迁移文件。
dnx ef migrations add products
这将创建一个名为“products”的迁移文件。技术上讲,这个名字可以是任何东西,但象征性地命名迁移文件是一种好习惯。现在让我们用迁移文件更新数据库。
dnx ef database update
现在您可以运行站点,您的 ProductCategory 功能应该可以正常工作了。仅为测试目的,我为产品类别制作了一个默认图片,将其保存在您的 /wwwroot/images 目录中,然后您可以将 CategoryImageUrl 设置为 "~/images/defaultImage.png”。
"
下一步 - 动态菜单
现在让我们让我们的产品类别发挥作用。让我们根据我们的产品类别来修改导航菜单,使其动态化。为此,我们将使用 ViewComponent 来为我们渲染菜单。首先,在解决方案资源管理器中,右键单击 ProductsIncorporated 文件夹,选择 Add->New Folder,并命名为“ViewComponents”。接下来,在新的“ViewComponents”文件夹上,右键单击并选择 Add->Class,并将其命名为“MenuViewComponent”。下面是我们的 MenuViewComponent 类应该是什么样子。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using ProductsIncorported.Models;
using System.Linq;
using Microsoft.Data.Entity;
namespace ProductsIncorported.ViewComponents
{
[ViewComponent(Name = "MenuViewComponent")]
public class MenuViewComponent : ViewComponent
{
private ApplicationDbContext db;
public MenuViewComponent(ApplicationDbContext context)
{
db = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var items = await GetItemsAsync();
return View(items);
}
private Task<List<ProductCategory>> GetItemsAsync()
{
return db.ProductCategory.Select(c => c).ToListAsync();
}
}
}
接下来,我们需要制作 ViewComponent 用于渲染自身的 Razor 视图。系统将查找 Views->Shared->Components->"Component Name" 目录中的此视图,所以让我们创建该目录结构。右键单击 Views->Shared 文件夹,创建一个名为 Components 的新文件夹。然后点击新的 Components 文件夹,创建一个名为 MenuViewComponent 的子文件夹。现在,右键单击新的 MenuViewComponent 文件夹,选择 Add->New Item。在对话框中选择 MVC View Page 并将其命名为 Default.cshtml。我们的视图页面只需要遍历模型列表,为每个项创建一个列表项。下面是 Default.cshtml 视图应该是什么样子。
@model IEnumerable<ProductCategory>
@foreach (var item in Model)
{
<li><a asp-controller="Products" asp-action="Details" asp-route-id="@item.Id">@item.CategoryName</a></li>
}
现在我们只需要在布局中调用这个视图组件。让我们打开布局视图,即 Views->Shared-> _Layout.cshtml。查找如下所示的部分。
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>....
<li>....
</ul>
@await Html.PartialAsync("_LoginPartial")
</div>
让我们用下面的 Invoke 调用替换所有 <li> 行。
@await Component.InvokeAsync("MenuViewComponent")
因此,上述部分现在看起来像这样。
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
@await Component.InvokeAsync("MenuViewComponent")
</ul>
@await Html.PartialAsync("_LoginPartial")
</div>
注意: 我们将 [ViewComponent] 注解添加到了我们的 MenuViewComponent 类中,并定义了名称“MenuViewComponent”,这就是我们在上面的 InvokeAsync 方法中调用的名称。此外,我们的组件类中的 Invoke 方法没有参数,但是,您可以根据需要向 Invoke 方法传递参数,并将它们包含在上面的 InvokeAsync 的 params args 调用中。
现在您可以看到这一点了。运行站点,您会看到菜单现在是空的。浏览到 _https://:port/Products_ 并创建一个产品类别,您会看到它被添加到菜单中,单击新的菜单项应该会带您到该类别的详细信息页面。
用户友好的链接
现在让我们对路由工作方式进行一些调整。您会注意到在地址栏中,当您单击菜单链接时,URL 的样子如下。
https://:port/Products/Details/1
换句话说,Id 是我们用于标识类别的值。这不是最用户友好的做法。您希望您的受众能够输入 ./Produts/Details/Stuff 或 ./Products/Details/Things,这在营销材料中看起来也更好。所以让我们对其进行一些调整,以便我们可以通过名称而不是 ID 来调用类别。
为此,我们需要在 Products 控制器中添加一个重载的 Details 方法,并向两个 Details 方法添加一些路由注解。首先,对于现有的 Details 方法,让我们添加一些路由注解,以便 MVC 引擎可以区分在处理请求时使用哪个方法。当前的 Details 方法签名需要更改为。
[ActionName("Details")]
[Route("Products/Details/{id}")]
public IActionResult Details(int? id)
{
.....
现在让我们添加一个重载的 Details 方法,该方法将接受一个字符串并根据该字符串匹配 Category Name 返回一个类别。
[ActionName("Details")]
[Route("Products/Details/{category}")]
public IActionResult Details(string category)
{
if (category == null || category == string.Empty)
{
return HttpNotFound();
}
ProductCategory productCategory = _context.ProductCategory.Single(m => m.CategoryName == category);
if (productCategory == null)
{
return HttpNotFound();
}
return View(productCategory);
}
注意: 我们必须为现有方法添加路由注解,这样它就不会匹配默认调用,这样引擎就会继续查找。此外,我们没有采用任何检查来确保每个 CategoryName 都唯一,因此有可能添加多个具有相同名称的类别,这意味着每次只返回第一个。您可能需要考虑在“创建”部分添加保护措施,以便类别保持唯一的名称。
现在我们只需要回到 MenuViewComponent 的 Default.cshtml 视图,并将 <li> 标签更改为。
asp-route-id="@item.Id"
现在应该是。
asp-route-category="@item.CategoryName"
好了,现在测试一下,您会看到引擎生成了更加用户友好的 URL。
现在让我们将后端与前端分开。假设我们不希望我们的受众修改我们的菜单结构,只希望我们的指定员工修改。当然,您会希望将编辑控件隔离在您的身份验证墙后面,但这超出了本教程的范围。但对于本指南,我们将创建一个单独的视图来向用户显示类别详细信息,我们称之为“Info”。因此,我们将向用户呈现一个 URL 结构 ./Products/Info/category,而不是 .Products/Details/category。
首先,让我们创建新的 Info 视图。在解决方案资源管理器中,在 Views->Products 文件夹上,右键单击并选择 Add->View。这将打开新的视图对话框。
我们将其命名为“Info”,选择 Details 模板,ProductCategory 模型类,将 Data context 保持原样,并确保选中 Use layout 并且布局为空。这将为我们创建新的 Info 视图,目前它基本上与 Details 视图相同,但我们将很快对其进行更改。但在那之前,让我们让它工作起来。我们需要在 ProductsController 中添加一个用于返回此视图的方法。
[HttpGet]
[ActionName("Info")]
[Route("Products/Info/{category}")]
public IActionResult Info(string category)
{
if (category == null || category == string.Empty)
{
return HttpNotFound();
}
ProductCategory productCategory = _context.ProductCategory.Single(m => m.CategoryName == category);
if (productCategory == null)
{
return HttpNotFound();
}
return View(productCategory);
}
最后,让我们更改 MenuViewComponent 视图 Default.cshtml,以便它调用正确的操作,更改 <li> 标签,这样。
asp-action="Details"
现在读作。
asp-action="Info"
就是这样,现在我们的新视图 Info 已经可以工作了。现在我们可以着手修改它以满足我们的需求。基本上,为了简单起见,我们只需要一个居中的标题,即类别名称、图片和描述。我们不需要 Details 模板中提供的控件链接。让我们将 Info.cshtml 更改为。
@model ProductsIncorported.Models.ProductCategory
@{
ViewData["Title"] = "Info";
}
<div>
<center>
<h2>@Model.CategoryName</h2>
<br />
<img src="@Model.CategoryImageUrl" alt="@Model.CategoryName" />
<br />
<p>@Model.Description</p>
</center>
</div>
Inline Editing
接下来,让我们为创建和编辑视图增加一点风格和可用性。而不是脚手架提供的默认视图,让我们将它们更改为模仿 Info 页面。这使得页面管理员能够立即了解页面在编辑时的样子,并提供了相当干净的审核体验。首先,让我们更改 Views->Products->Create.cshtml 视图中的标记。
<form asp-action="Create">
<div class="form-horizontal">
<center>
<br />
<input id="categoryName" asp-for="CategoryName" class="form-control" placeholder="Caption" />
<br />
<img src="@Model.CategoryImageUrl" alt="@Model.CategoryName" />
<br />
<textarea id="description" asp-for="Description" class="form-control" rows="3" placeholder="Category description text"></textarea>
<br /><br />
<a class="btn btn-default" asp-action="Index">Cancel</a>
<button class="btn btn-default" type="submit">Save</button>
</center>
</div>
</form>
这里使用的 CSS 类在 boostrap CSS 中定义。这是一个很好的起点,对于普通表单来说,这就是您所需要的。但是,让我们稍微调整一下我们两个文本控件的样式,让它们看起来像原始页面,只是可编辑。添加以下样式部分。
<style>
#categoryName {
border: 0;
box-shadow: none;
text-align: center;
font-size: 18pt;
min-width: 500px;
}
#description {
border: 0;
box-shadow: none;
text-align: left;
font-size: 12pt;
min-width: 800px;
}
</style>
根据您的页面样式,您可能需要对样式进行其他更改以使其匹配。
图片库
现在我们需要做的最后一件事是让管理员能够更改图片。为此,我们将使用几个第三方工具。Bootbox 允许我们弹出模态对话框,我们可以用自定义 HTML 填充它。在这里,我们将使用 Image-Picker 工具将图片库放入 bootbox 模态框中,允许用户选择服务器上已有的图片。供参考,以下是我们将在使用的两个工具的网站。
Bootbox
http://bootboxjs.com/
Image-Picker
https://rvera.github.io/image-picker/
首先,我们需要为用户提供一个启动图片库控件的方法,这可以通过多种方式完成,您可以创建另一个按钮,但这会破坏“内联”外观,所以在这个例子中,我们将实际将图片显示制作成一个启动 bootbox 对话框的按钮。为此,我们将 <img> 标签包装在 <a> 标签中,并使用 jQuery 处理单击事件。所以让我们将 <img> 更改为如下。
<a id="imageSelectButton"><img src="@Model.CategoryImageUrl" alt="@Model.CategoryName" /></a>
让我们在视图页面的底部添加一个脚本部分来处理我们的点击。
<script>
$(document).on('click', '#imageSelectButton', function(e) {
});
</script>
让我们暂时将此留空。您可以只需 alert('Working'); 在函数中进行测试,以确保 jQuery 正常工作。让我们暂时切换一下。为了让我们的图片库正常工作,我们需要有一个我们想要用它填充 image-picker 的目录中的图片列表。为此,我们将让我们的 Controller 向我们传递一个文件名列表,我们将用它来填充一个隐藏的 <select> 控件。首先,让我们将文件名列表传递到我们的 View。打开 ProductsController 类。首先,我们需要添加 IApplicationEnvironment 的一个实例,我们将用它来将我们的图像目录映射到服务器目录。基本上,这是实现旧 Server.MapPath() 调用的一种新方法。添加 Microsoft.Extensions.PlatformAbstractions 的 using 语句,这是包含我们需要的 IApplicationEnvironment 的库。创建一个私有实例,并将 ProductsController 构造函数更改为包含 IApplicationEnvironment 参数,并将其设置为实例。引擎在调用构造函数时会知道传递此参数。
private IApplicationEnvironment _environment;
public ProductsController(IApplicationEnvironment env, ApplicationContext context)
{
_environment = env;
.....
现在让我们修改 Create() 方法如下。
public IActionResult Create()
{
List<SelectListItem> imageList = new List<SelectListItem>();
string path = _environment.ApplicationBasePath + "\\wwwroot\\images";
DirectoryInfo pathDir = new DirectoryInfo(path);
foreach(FileInfo f in pathDir.GetFiles("*.jpg"))
{
SelectListItem newItem = new SelectListItem();
newItem.Text = f.Name;
newItem.Value = f.FullName.Replace(path, "/images").Replace("\\", "/");
imageList.Add(newItem);
}
foreach(FileInfo f in pathDir.GetFiles("*.png"))
{
SelectListItem newItem = new SelectListItem();
newItem.Text = f.Name;
newItem.Value = f.FullName.Replace(path, "/images").Replace("\\", "/");
imageList.Add(newItem);
}
ViewData["ImageList"] = imageList;
ProductCategory newCat = new ProductCategory();
newCat.ProductImageUrl = "~/images/imagePlaceholder.png";
return View(newCat);
}
注意: 确保添加 System.IO 的 using 语句。
我们在这里做的是创建一个 SelectListItem 列表,包含 ~/images 目录中的所有文件。然后我们将此对象添加到 ViewData 容器中,这是一个用于存储我们需要传递给 View 的对象的容器。现在我们有了传递到视图的文件列表,让我们用它来填充一个隐藏的 <select> 控件。然后我们将使用 jQuery 将这些选项传递到我们将在对话框中填充的图片库中。让我们现在就制作这个隐藏的 <select>。您真的可以把它放在任何地方,但让我们把它添加到 <img> 标签的上方,它需要看起来像这样。
<select id="imageList" asp-for="@empty" asp-items="@(ViewData["ImageList"] as List<SelectListItem>)"></select>
注意 @empty。这是一个变通方法,让我们能够使用 asp-items 标签助手。如果您没有指定 asp-for,您就无法使用 asp-items,即使 asp-for 不必是任何东西。在文档的顶部,我们只需要声明一个名为“empty”的空整数,在 ViewData["Title"] 行下方执行此操作。
ViewData["Title"] = "Create";
int? empty = null;
最后,为此,我们需要隐藏 <select>,我们将通过调整其样式来做到这一点,所以我们在 <style> 部分添加以下内容。
#imageList {
display: none;
}
您现在可以测试一下,运行站点,然后浏览到 /Products/Create 页面,并查看其页面源。您会在源中看到一个 <select> 控件,其中包含我们指定的目录中每个文件名的选项。现在让我们回到我们的 jQuery 点击处理程序,在那里我们将添加我们的 bootbox 对话框,并用 image-picker 填充它。首先,让我们确保我们已经下载了 bootbox 和 image-picker 文件。将它们放在您的 /wwwroot/js 文件夹中,并将脚本导入添加到页面底部的 @section Scripts{} 块中。您还需要导入 bootstrap.js 库,因为 bootbox 依赖于它。此导入应在 bootbox 导入之前完成。
<script src="~/lib/boostrap/dist/js/bootstrap.js"></script>
<script src="~/js/image-picker.js"></script>
<script src="~/js/bootbox.js"></script>
探索 bootbox 网站,了解它的所有功能,我们将使用 bootbox.dialog() 方法,我们将向其传递一个标题、消息(对话框的 HTML)和一个 OK 按钮。首先,让我们生成一个字符串作为“message”参数传递,这将是一个 <select> 标签,它将成为我们的 image-picker。
var selectTag = '<div style="height: 500px; overflow-y: scroll;"><select id="imageSelectList" class="image-picker show-labels show-html">';
var index = 1;
$('#imageList').find('option').each(function () {
selectTag = selectTag + '<option data-img-label="' + $(this).text() + '" data-img-src="' + $(this).val() + '" value="' + index + '">' + $(this).text() + '</option>';
index++;
});
selectTag = selectTag + '</select></div>';
花点时间查看 image-picker 网站,了解它的工作原理。上面我们所做的是遍历隐藏的 <select> 标签的列表项,并使用它们来填充我们将放入对话框的新 <select>。我们需要使用的特定于 image-picker 的参数是 data-img-label,这是将显示在每个缩略图上的标签,对于这些标签,我们只使用文件名。然后我们需要 data-img-src,这是图片的 URL,然后 value 需要是一个递增的索引,从 1 开始。我们将其包含在其中的 <div> 允许我们在对话框中放置滚动条,所以如果文件列表很大,用户就可以滚动它。所以现在让我们在 click 函数中进行 bootbox.dialog() 调用。
$(document).on('click', '#imageSelectButton', function (e) {
bootbox.dialog({
title: 'Available Images',
message: selectTag,
buttons: {
success: {
label: "Select",
className: "btn-success",
callback: function () {
$('#imageSelectionControl').attr('src', "/images/" + $('#imageSelectList option:selected').text());
$('#imageUrlField').val("~/images/" + $('#imageSelectList option:selected').text());
}
}
}
}).find('div.modal-dialog').addClass('imageGalleryContainer');
$('#imageSelectList').imagepicker();
});
现在进入 _Layout.cshtml 并导入 image-picker.css 样式表。请务必从 image-picker 下载此文件,并将其放入 /wwwroot/css 文件夹中。
<link rel="stylesheet" type="text/css" href="~/css/image-picker.css" />
现在,如果您注意到,在 bootbox 按钮回调中,我们指示将选定的图片 URL 设置为几项。其中一项是页面上图片的“src”属性,所以为了让它工作,我们需要将“id”imageSelectionControl 添加到 <img> 标签中。
<img id="imageSelectionControl" src="@Model.CategoryImageUrl" alt="@Model.CategoryName" />
最后,我们需要另一个隐藏的控件,一个 <input>,它将跟踪选定的图片 URL,这是我们在对话框按钮回调中设置 val() 的另一个控件,imageUrlField。让我们现在就创建它,同样,它将保持隐藏,所以不用太担心把它放在哪里,我们会把它放在 <a><img> 标签的正下方。
<input asp-for="CategoryImageUrl" id="imageUrlField" readonly="readonly" />
然后让我们对其进行样式设置,使其隐藏,添加样式标签。
#imageUrlField {
display: none;
}
最后要提的一点是,如果您注意到,我们在 bootbox.dialog() 调用末尾添加了一个 .find().addClass() 调用。这是一个设置 bootbox 对话框宽度的变通方法,在这种情况下,我们将其设置为 85%,所以让我们添加样式标签。
.imageGalleryContainer {
width: 85%;
}
我们还添加一个样式,当鼠标悬停在图片上时显示链接光标,这样用户就知道它是可点击的。
#imageSelectionControl:hover {
cursor: pointer;
}
就这样。测试一下这些控件,您可以看到每次选择新图片并单击 Select 对话框按钮时图片都会发生变化。而且,当您保存类别时,它会保存到新的图片 URL。
进一步扩展
现在您可以进一步扩展,将 Edit 视图更改为与 Create 视图执行相同操作。您必须以与我们修改 Create 方法相同的方式更新 Controller 中的 Edit 方法,设置 ViewData 以包含文件列表。
此外,我们可以稍微调整一下 image-picker,以便图片缩略图保持在特定的尺寸要求内。您可以添加这两个样式。
.thumbnails li {
text-align: center;
vertical-align: middle;
}
.thumbnails li img {
max-width: 128px;
max-height: 128px;
width: 128px;
}
这将使标签居中对齐,并将缩略图限制在 128 像素宽。
关注点
- Image Picker
- Bootbox
- 内联编辑
- MVC
- 路由