12.SportsStore之产品管理

  • 时间:2019-06-19
  • 作者:Charles
  • 热度:4437

本文继续构建SportsStore应用程序,为网站管理员提供一个管理产品的方法。

管理条目集合的惯例,是向用户显示两种形式的页面,一个列表页和一个编辑页。页面合起来可以让用户创建、读取、更新和删除集合中的条目。这些动作统称为“CRUD”,开发人员往往需要实现CRUD,因此VS通常会设法对此提供帮助,以便生成具有CRUD操作动作方法的MVC控制器,同时也提供对这些操作进行支持的视图。

一:创建CRUD控制器

本小节将创建一个新的控制器来处理这些管理功能。右击SportsStore.WebUI项目的Controllers文件夹,从弹出菜单中选择添加控制器,将该控制器命名为AdminController,模板选择为空的MVC控制器。添加一个动作方法,用来显示存储库中的所有产品,将其方法命名为Index,如下所示。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstrace;
using SportsStore.Domain.Entities;
 
namespace SportsStore.WebUI.Controllers
{
    [Authorize]
    public class AdminController : Controller
    {
        private IProductRepository repository;
        public AdminController(IProductRepository repo)
        {
            repository = repo;
        }
        // GET: Admin
        public ActionResult Index()
        {
            return View(repository.Products);
        }
}

二:创建新的布局

本例打算创建一个新的Razor布局,以用于SportsStore的管理视图。右击SportsStore.WebUI项目的Views/Shared文件夹,选择添加-MVC5布局页,将其命名为_AdminLayout.cshtml,如下所示。

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - 后台管理</title>
        @Styles.Render("~/Content/css")
        @Styles.Render("~/Content/admincss")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div>
            <div>
                <button type="button" data-toggle="collapse" data-target=".navbar-collapse">
                    <span></span>
                    <span></span>
                    <span></span>
                </button>
                @Html.ActionLink("运动商城", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("首页", "Index", "Home")</li>
                </ul>
            </div>
 
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - 运动商城</p>
        </footer>
    </div>
 
        @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>

三:实现List视图

现在已经创建了一个新的布局页,下面可以对项目添加一个视图,用于Admin控制器的Index动作方法。在Index方法中右击,选择添加视图,在添加视图对话框中,视图名称命名为Index,模板选择List,模型类选择Product,选中使用布局页复选框,布局选择刚刚新建的_AdminLayout.cshtml文件,如下所示。

                                             

点击添加,稍等片刻VS将创建视图如下所示。

@model IEnumerable<SportsStore.Domain.Entities.Product>
 
@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
 
<h2>Index</h2>
 
<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Description)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Price)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Category)
        </th>
        <th></th>
    </tr>
 
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Description)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Price)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Category)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.ProductID }) |
            @Html.ActionLink("Details", "Details", new { id=item.ProductID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.ProductID })
        </td>
    </tr>
}
 
</table>

VS会考察视图模型对象的类型,并根据该模型所定义的属性,生成一些表格形式的元素。启动应用程序,导航到Admin/Index地址,我们将会看到如下渲染结果。

List模板为用户做了很好的设置工作。此时得到了Product类中各个属性的表格列,以及进行CRUD操作的链接,这些链接指向同一个控制器的各个动作方法。但是这些标记有些冗长,而且样式也不美观,我们需要重新编辑此文件,如下所示。

@model IEnumerable<SportsStore.Domain.Entities.Product>
 
@{
    ViewBag.Title = "产品管理";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
 
<h2>产品管理</h2>
<hr />
<div>
    <table class="table table-hover">
        <thead>
            <tr>
                <th>ID</th>
                <th>名称</th>
                <th>价格</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model)
            {
                <tr>
                    <td>@item.ProductID</td>
                    <td>@Html.ActionLink(item.Name,"Edit",new { item.ProductID})</td>
                    <td>@item.Price.ToString("c")</td>
                    <td>
                        @using (Html.BeginForm("Delete","Admin"))
                        {
                            @Html.Hidden("ProductID",item.ProductID)
                            <input type="submit" value="删除" class="btn btn-danger btn-sm" />
                        }
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>
<p>
    @Html.ActionLink("新建产品", "Create")
</p>

修改后的视图以一种更紧凑的形式表现了相关信息,它忽略了Product类的一些属性,并用一种不同的办法展示了指向产品的链接。启动应用程序,我们将会看到如下渲染。

四:编辑产品

为了提供创建和更新特性,将添加一个产品编辑页面。此工作有两个部分:显示一个让管理员能够修改产品属性的页面;添加一个动作方法,它能够在提交时对这些修改进行处理。

4.1创建Edit动作方法

在AdminController类中添加Edit方法,如下所示。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstrace;
using SportsStore.Domain.Entities;
 
namespace SportsStore.WebUI.Controllers
{
    public class AdminController : Controller
    {
        private IProductRepository repository;
        public AdminController(IProductRepository repo)
        {
            repository = repo;
        }
        // GET: Admin
        public ActionResult Index()
        {
            return View(repository.Products);
        }
        public ActionResult Edit(int productId)
        {
            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
            return View(product);
        }
    }
}

这个简单的方法找出与productId参数对应ID的产品,并把它作为一个视图模型对象进行传递。

4.2创建Edit视图

右击Edit方法,选择添加视图,视图命名为Edit,模型类选择Product。也可以使用Edit模板来创建视图,本例不再演示。新建的Edit视图如下所示。

@model SportsStore.Domain.Entities.Product
@{
    ViewBag.Title = Model.ProductID > 0 ? "产品编辑" : "产品添加";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h2>@(Model.ProductID > 0 ? "产品编辑" : "产品添加")</h2>
<hr />
<div>
    @using (Html.BeginForm())
    {
        @Html.EditorForModel()
       <input type="submit" value="保存" class="btn btn-primary" />
        @Html.ActionLink("取消并返回列表", "Index")
    }
</div>

此例并未手工为每个标签和输入项编写标记,而是调用了Html.EditorForModel辅助器方法。这个方法要求MVC框架创建编辑界面,这是通过探测其模型类型来实现的。

运行应用程序并导航到/Admin/Index,点击一个产品名,将会看到如下所示的页面。

EditorForModel方法很方便,但它产生的结果并不令人十分满意。比如:我们并不希望管理员可以看到或编辑ProductID属性;Description的输入框也太小了;输入框名也是直接显示的模型类属性名。

解决以上问题,我们可以运用注解属性,将注解属性运用于模型类的属性上,以影响Html.EditorForModel方法的输出,编辑Product类文件如下所示。

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace SportsStore.Domain.Entities
{
    [Serializable()]
    public class Product
    {
        [HiddenInput(DisplayValue =false)]
        public int ProductID { get; set; }
        [DisplayName("名称")]
        public string Name { get; set; }
        [DataType(DataType.MultilineText)]
        [DisplayName("简介")]
        public string Description { get; set; }
        [DisplayName("价格")]
        public decimal Price { get; set; }
        [DisplayName("类别")]
        public string Category { get; set; }
    }
}

HiddenInput注解属性告诉MVC框架,将该属性渲染为隐藏的表单元素,DisplayName注解属性设置编辑框的名称,而DataType注解属性能够指示如何显示或编辑一个值。本例选择了MultilineText选项。此时再次运行应用程序,打开一个产品的编辑页面,将会看到如下渲染。

4.3更新产品存储库

在能够处理编辑之前,需要增强产品存储库,才能保证所作的修改。首先要对IProductRepository接口添加一个新的方法,如下所示。

using System.Linq;
using SportsStore.Domain.Entities;
 
namespace SportsStore.Domain.Abstrace
{
    public interface IProductRepository
    {
        IQueryable<Product> Products { get; }
        void SaveProduct(Product product);
    }
}
然后将这个方法添加到存储库的EF实现上,编辑EFProductRepository类,如下所示。
using SportsStore.Domain.Abstrace;
using SportsStore.Domain.Entities;
using System.Linq;
 
namespace SportsStore.Domain.Concrete
{
    public class EFProductRepository:IProductRepository
    {
        private EFDbContext context = new EFDbContext();
 
        public IQueryable<Product> Products
        {
            get { return context.Products; }
        }
        public void SaveProduct(Product product)
        {
            if (product.ProductID == 0) context.Products.Add(product);
            else
            {
                Product dbEntry = context.Products.Find(product.ProductID);
                if (dbEntry!=null)
                {
                    dbEntry.Name = product.Name;
                    dbEntry.Description = product.Description;
                    dbEntry.Price = product.Price;
                    dbEntry.Category = product.Category;
                    dbEntry.ImageData = product.ImageData;
                    dbEntry.ImageMimeType = product.ImageMimeType;
                }
            }
            context.SaveChanges();
        }
    }
}

如果ProductID为0,这个SaveChanges方法实现会将一个产品加入存储库;否则会将任何修改运用于数据库中已存在的物品。

4.4处理Edit的POST请求

此刻已经做好了准备,以便在Admin控制器中实现一个重载的Edit方法,它将在管理员点击保存按钮时处理POST请求,新方法如下所示。

using SportsStore.Domain.Abstrace;
using SportsStore.Domain.Entities;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace SportsStore.WebUI.Controllers
{
    public class AdminController : Controller
    {
        private IProductRepository repository;
        public AdminController(IProductRepository repo)
        {
            repository = repo;
        }
        // GET: Admin
        public ActionResult Index()
        {
            return View(repository.Products);
        }
        public ActionResult Edit(int productId)
        {
            Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId);
            return View(product);
        }
        [HttpPost]
        public ActionResult Edit(Product product)
        {
            if (ModelState.IsValid)
            {
                repository.SaveProduct(product);
                TempData["msg"] = string.Format("{0}已成功保存", product.Name);
                return RedirectToAction("Index");
            }
            else return View(product);
        }
    }
}

在存储库保存了修改后,用TempData特性保存了一条消息,它类似于之前已经使用过的会话数据和ViewBag特性。与会话数据的关键差异是,TempData在HTTP请求结束时会被删除。

4.5显示确认消息

本例打算在_AdminLayout.cshtml布局文件中处理TempData存储的消息。通过在布局文件中处理消息,可以在任何使用此布局的视图中创建消息,而不需要创建额外的Rozar块。编辑_AdminLayout.cshtml如下所示。

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - 后台管理</title>
        @Styles.Render("~/Content/css")
        @Styles.Render("~/Content/admincss")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div>
            <div>
                <button type="button" data-toggle="collapse" data-target=".navbar-collapse">
                    <span></span>
                    <span></span>
                    <span></span>
                </button>
                @Html.ActionLink("运动商城", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("首页", "Index", "Home")</li>
                </ul>
            </div>
        </div>
    </div>
    <div class="container body-content">
        @if (TempData["msg"] != null)
        {
            <div class="alert alert-info alert-dismissable" style="margin-top:20px;">
                <button type="button" data-dismiss="alert"
                        aria-hidden="true">
                    &times;
                </button>
        @TempData["msg"]</div>
        }
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - 运动商城</p>
        </footer>
    </div>
        @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>

现在有了对产品编辑进行测试的所有元素,运行应用程序,导航到Admin/Index,编辑一个产品,点击保存按钮,系统将返回到列表视图,并显示TempData消息,如下所示。

如果刷新页面,这条消息将会消失,这是因为TempData在被读取时就被删除了。

4.6添加模型验证

通常情况下,需要对模型实体添加验证规则,编辑Product类文件,如下所示。

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace SportsStore.Domain.Entities
{
    [Serializable()]
    public class Product
    {
        [HiddenInput(DisplayValue =false)]
        public int ProductID { get; set; }
        [DisplayName("名称")]
        [Required(ErrorMessage ="请输入{0}")]
        public string Name { get; set; }
        [DataType(DataType.MultilineText)]
        [DisplayName("简介")]
        [Required(ErrorMessage = "请输入{0}")]
        public string Description { get; set; }
        [DisplayName("价格")]
        [Required(ErrorMessage = "请输入{0}")]
        [Range(0.01, double.MaxValue, ErrorMessage = "{0}必须在{1}和{2}之间")]
        public decimal Price { get; set; }
        [DisplayName("类别")]
        [Required(ErrorMessage = "请输入{0}")]
        public string Category { get; set; }
    }
}

使用Html.EditorForModel辅助器方法来创建Form元素以编辑Product时,MVC框架添加了显示内联的验证错误所需的所有标记和CSS。当编辑一个产品而不符合验证规则时,会出现如下所示的界面。

4.7启用客户端验证

此时,只有当管理员把编辑提交给服务器时,才会运用数据验证。大多数Web用户希望,如果输入的数据有问题,要立即得到反馈。数据在浏览器中用Javascript进行检查,MVC框架可以根据运用于域模型类的数据注解来执行客户端验证。

这一特性是默认可用的,但它尚不会生效,因为还没有添加对所需的JavaScript库的链接。编辑_AdminLayout.cshtml如下所示。

<div class="container body-content">
        @if (TempData["msg"] != null)
        {
            <div class="alert alert-info alert-dismissable" style="margin-top:20px;">
                <button type="button" data-dismiss="alert"
                        aria-hidden="true">
                    &times;
                </button>
        @TempData["msg"]</div>
        }
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - 运动商城</p>
        </footer>
    </div>       
    @Scripts.Render("~/bundles/jquery", "~/bundles/jqueryval", "~/bundles/bootstrap")
    @RenderSection("scripts", required: false)

通过Scripts.Render添加jquery.validate库,客户端验证将对管理视图生效。显示给用户错误消息的外观是相同的,因为服务器端验证所使用的CSS的class也由客户端验证所使用。客户端验证得到的相应是及时的,并且不需要把请求发送到服务器。某些情况下不希望在客户端验证时,可以使用以下语句。

HtmlHelper.ClientValidationEnabled = false;

HtmlHelper.UnobtrusiveJavaScriptEnabled = false;

如果把上面的语句放在一个视图或一个控制器中,那么客户端验证只对当前的动作失效。要禁用整个应用程序的客户端验证,可以在Global.asax的Application_Start方法中使用上面的语句,或是把这些值运用于Web.config文件,代码如下。

<configuration>
  <appSettings>
<add key="ClientValidationEnabled" value="false" />
<add key="UnobtrusiveJavaScriptEnabled" value="false" />
</appSettings>
<connectionStrings>

4.8 Scripts.Render和Styles.Render

在_AdminLayout.cshtml以及之前的默认布局页中,我们使用了Scripts.Render和Styles.Render特性来引用js库和CSS文件。

Scripts.Render是asp.net mvc用于优化页面请求的技术。基本功能与在页面中直接书写<script>标签是一样的。但是通过@script.Render方法,你可以预定义一组js文件,在最终页面上,asp.net mvc自身会采用比较优化的压缩技术和缓存技术,将多个js压缩优化并且整合为1个体积较小的js,但对于外部使用来说,功能是不会出现任何区别的,从而提高了页面体验。而压缩和缓存的过程都是通过框架自动实现的,你只需要按照缩进和自己的习惯写优雅的js代码即可。

使用Scripts.Render或Styles.Render特性,首先要在App_Start 里面BundleConfig.cs 文件里面添加要包含的js或css文件,BundleConfig就是一个微软新加的一个打包的配置类,BundleConfig用来Add 各种Bundle,BundleConfig配置信息如下:

using System.Web;
using System.Web.Optimization;
namespace SportsStore.WebUI
{
    public class BundleConfig
    {
        // 有关捆绑的详细信息,请访问 https://go.microsoft.com/fwlink/?LinkId=301862
        public static void RegisterBundles(BundleCollection bundles)
        {
            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));
 
            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                        "~/Scripts/jquery.validate*"));
 
            // 使用要用于开发和学习的 Modernizr 的开发版本。然后,当你做好
            // 生产准备就绪,请使用 https://modernizr.com 上的生成工具仅选择所需的测试。
            bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                        "~/Scripts/modernizr-*"));
 
            bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                      "~/Scripts/bootstrap.js",
                      "~/Scripts/respond.js"));
            bundles.Add(new StyleBundle("~/Content/css").Include(
                      "~/Content/bootstrap.css",
                      "~/Content/site.css"));
            bundles.Add(new StyleBundle("~/Content/admincss").Include(
                       "~/Content/Admin.css"));
        }
    }
}

然后在视图文件中使用Scripts.Render()输出脚本包,Styles.Render()输出样式包:

Script文件引用:@Scripts.Render(virtualPath[,virtualPath1][,virtualPath2][,...])

CSS文件引用:@Styles.Render(virtualPath[,virtualPath1][,virtualPath2][,...])

如下所示:

@Scripts.Render("~/bundles/jquery", "~/bundles/jqueryval", "~/bundles/bootstrap")
@Styles.Render("~/Content/admincss")

五:创建新产品

下一步将实现Create动作方法,这是在产品列表页中“新建产品”链接所指定的方法。对AdminController类添加Create方法,如下所示。

public ActionResult Create()
        {
            return View("Edit", new Product());
        }

这个Create方法并不渲染它的默认视图,而是指明应该使用Edit视图。我们还需要修改Edit视图中的Html.BeginForm辅助器方法,以使生成表单的目标始终是Admin控制器的Edit方法,如下所示。

@model SportsStore.Domain.Entities.Product
@{
    ViewBag.Title = Model.ProductID > 0 ? "产品编辑" : "产品添加";
    Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
 
<h2>@(Model.ProductID > 0 ? "产品编辑" : "产品添加")</h2>
<hr />
 
<div>
    @using (Html.BeginForm("Edit","Admin"))
    {
        @Html.EditorForModel()
       <input type="submit" value="保存" class="btn btn-primary" />
        @Html.ActionLink("取消并返回列表", "Index")
    }
</div>

经过修正之后,此表单将总是被提交给Edit动作,而不管渲染它的是哪个动作。

六:删除产品

添加对物品进行删除的支持相当简单,首先把一个方法添加到IProductRepository接口,如下所示。

using System.Linq;
using SportsStore.Domain.Entities;
 
namespace SportsStore.Domain.Abstrace
{
    public interface IProductRepository
    {
        IQueryable<Product> Products { get; }
        void SaveProduct(Product product);
        Product DeleteProduct(int productID);
    }
}

下一步,在EFProductRepository中实现这个方法,如下所示。

using SportsStore.Domain.Abstrace;
using SportsStore.Domain.Entities;
using System.Linq;
 
namespace SportsStore.Domain.Concrete
{
    public class EFProductRepository:IProductRepository
    {
        private EFDbContext context = new EFDbContext();
        public IQueryable<Product> Products
        {
            get { return context.Products; }
        }
        public void SaveProduct(Product product)
        {
            if (product.ProductID == 0) context.Products.Add(product);
            else
            {
                Product dbEntry = context.Products.Find(product.ProductID);
                if (dbEntry!=null)
                {
                    dbEntry.Name = product.Name;
                    dbEntry.Description = product.Description;
                    dbEntry.Price = product.Price;
                    dbEntry.Category = product.Category;
                    dbEntry.ImageData = product.ImageData;
                    dbEntry.ImageMimeType = product.ImageMimeType;
                }
            }
            context.SaveChanges();
        }
        public Product DeleteProduct(int productID)
        {
            Product dbEntry = context.Products.Find(productID);
            if (dbEntry!=null)
            {
                context.Products.Remove(dbEntry);
                context.SaveChanges();
            }
            return dbEntry;
        }
    }
}

最后一步是在Admin控制器中实现一个Delete动作方法,这个动作方法应当只支持POST请求,如下所示。

 [HttpPost]
        public ActionResult Delete(int productID)
        {
            Product deleteProduct = repository.DeleteProduct(productID);
            if (deleteProduct != null) TempData["msg"] = string.Format("成功删除{0}", deleteProduct.Name);
            return RedirectToAction("Index");
        }

现在运行应用程序,点击一个产品的删除按钮,就可以看到新功能已经起作用了。

六:小结

本文介绍了管理能力,并展示了如何实现CRUD操作,让管理员能够创建、读取、更新和删除存储库中的产品。



博主声明

1、本博客主要为原创文章,转载请注明出处。

2、部分文章来自网络,已注明出处,如有侵权请与本人联系。

3、如果文章内容有误,或者您有其他更好的意见、建议请给我留言,我会及时处理!