目录
介绍
软件要求
创建项目
Index页面
样式
品牌
部分视图
搜索产品部分视图
Basket视图
Basket部分视图
登记视图
结帐视图
通知视图
JSON产品加载
应用导航
结论
在本文中,我将展示如何在Visual Studio的帮助下使用ASP.NET Core创建基本的电子商务应用程序流。
本文是演示可在ASP.NET核心电子商务应用程序中实现的各种实践,模式,技术和框架的系列文章中的第一篇,同时我们逐步实现构建微服务解决方案的最终目标。
也就是说,如果您对本文中没有找到任何真正的微服务感到有点失望,那是因为我想要涵盖的主题太多了,仅仅在一篇文章中就不可能讨论所有这些问题(或者讨论它们,而不仅仅是表面上的问题)。此外,我不会立即提供微服务,因为我希望以“渐进式”逐步方法工作,重构和推进代码库,因为我们将继续前进。请耐心等待,享受骑行乐趣。
由于这只是整个系列的第一篇文章,我想列举下一部分的设想主题:
我们可以看到,有很多主题要涵盖。虽然部件已编号,但这仅用于计数目的。事实上,实际的顺序可以随着我们的进步而改变。
该项目将使用Visual Studio Community创建(这可以通过Visual Studio Code甚至命令行工具完成),选择MVC项目模板。
MVC代表模型——视图——控制器,它现在是一种无处不在的软件架构模式,用于在应用关注点分离原则的同时构建用户界面。
模型部分指的是承载对象的数据,负责保存在对外用户界面中显示的信息,以及验证,收集和传输用户键入的信息到应用程序后端。
视图部分负责呈现/显示用户界面组件。通常,这些在术语中称为网页,但实际上网页在技术上是一整套HTML文件(包括页眉,正文和页脚),图像,图标,CSS样式表,JavaScript代码等。单个视图可能会呈现所有网页,但通常每个视图仅负责内部页面内容。
控制器是负责处理对一组视图的传入请求,决定需要为视图提供哪些数据,以及请求和准备这些数据以及相应地调用视图的组件。控制器还将处理数据违规,并在需要时将应用程序重定向到错误页面。
因此,让我们开始使用Visual Studio创建一个新的ASP.NET Core MVC项目。
首先,我们从Visual Studio菜单中单击新项目,选择Web Application选项。
图1:新建项目对话框
这将打开向导窗口,我们必须在其中选择Web应用程序(模型——视图——控制器)选项。请务必取消选择“为HTTPS配置”选项,因为为了简单起见,我们不使用安全的HTTP(HTTPS),至少现在不使用。
图2:新的ASP.NET核心Web应用程序选择器
一旦从选定的MVC模板加载项目,我们就可以运行(通过按F5键),现在我们可以在我们喜欢的Web浏览器中看到应用程序的主页打开。
图3:运行应用程序主页
上图显示了一个非常简单的Web应用程序。新项目已经为我们提供了基本MVC架构所需的文件。
现在,我们在谈论哪些文件?让我们检查Visual Studio中的解决方案树:
图4:ASP.NET Core MVC项目结构
请注意上图中我们如何为所有MVC部件提供项目文件夹:Model,View和Controller。
但是我们的ASP.NET核心MVC应用程序是如何启动的?与每个.NET应用程序一样,可执行文件具有入口点,该入口点必须是包含在Program类中的Main()方法。
在ASP.NET Core应用程序中,该Main()方法必须设置并启动Web主机,即Web应用程序的主机。
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup();
}
清单1:Program类
正如我们在这里看到的那样,调用WebHost.CreateDefaultBuilder()方法以便可以创建webhost,但由于需要配置它,我们还必须调用UseStartup()以传递负责webhost配置的Startup 类名。让我们看看这个类是如何工作的以及它将如何在我们的应用程序中使用。
Startup结构简单。它只包含两种方法:
在此上下文中,“服务”是可以添加的任何组件,以便为我们的应用程序提供特定功能,例如:MVC,日志记录,数据库,身份验证,cookie,会话等。
这些组件也称为“中间件”,可以是“管道”的一部分。每个中间件决定是否将请求传递给管道中的下一个组件,并且可以包括要在管道中的后续组件之前或之后执行的算法。
通常,名为“MyService” 的服务将在我们的Startup类中引用两次:
我们来看看Startup类中的方法。第一个方法是ConfigureServices(),其只配置cookie策略选项并将MVC服务添加到应用程序:
public class Startup
{
...
// This method gets called by the runtime. Use this method
// to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure(options =>
{
// This lambda determines whether user consent
// for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
...
清单2:Startup类中的ConfigureServices()方法
然后,Configure方法定义了一组“Use- Service”方法引用的中间件:
public class Startup
{
...
// This method gets called by the runtime.
// Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days.
// You may want to change this for production scenarios,
// see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
清单3:Startup类中的Configure()方法
在这里,我们对要添加到请求管道的每个服务进行简短描述:
我们将在我们的商店中使用有限的30种产品。对于每一个,我们都有一个图像,要添加到wwwroot项目文件夹内的 /images/catalog文件夹中。
图5:产品catalog图像
这些产品将在主页内的“catalog”视图中显示。此catalog显示为按每个产品类别分组的一组产品。每个类别都有一个名为“Carousel” 的Bootstrap 4组件,它可以按4个产品组自动旋转category产品。
@{
ViewData["Title"] = "Home Page";
}
@for (int category = 0; category < 6; category++)
{
Category Name
}
清单4:catalogIndex视图
这是一小步,但由于Bootstrap 4不再带有icon fonts (gliphicon),所以我们自己安装它。
Visual Studio允许我们安装客户端库,例如Font Awesome,一种流行的图标字体包。
图6:添加客户端库菜单
图7:添加客户端库对话框
既然已经安装了字体文件,我们必须在_Layout.cshtml文件中引用它们:
清单5:添加Font Awesome引用到_Layout.cshtml文件
让我们看看如何添加我们的第一个图标。在Home/Index.cshtml视图中,我们添加了一个带有fa fa-shopping-cart类的HTML元素。
Add to basket
清单6:在catalog的Index视图中使用Font Awesome图标
这将自动显示“添加到购物篮”按钮左侧的购物车图标。
再次运行应用程序,我们看到如何呈现购物车图标:
图8:Font Awesome icons
通过打开_Layout.cshtml文件,我们可以使用我们公司的名称更改品牌。
© 2019 - The Grocery Store - Privacy
清单7:在_Layout.cshtml中更改公司名称
现在,由于默认的ASP.NET Core MVC模板不包含品牌,我们自己包含它。
我们还必须在顶部栏中包含公司logo,首先为导航栏定义背景CSS规则:
图9:logo.png文件
a.navbar-brand {
...
background: url('../images/logo.png');
...
}
清单8:在导航栏背景中定义公司logo
如果你看看我们的catalog index Razor文件,你会发现它变得庞大而复杂,这可能会影响其内容的可读性和理解。
使用ASP.NET Core,我们可以使用部分视图轻松地将大型标记文件(例如我们的catalog视图)拆分为更小的组件。
部分视图是一个Razor文件(.cshtml),它将HTML元素呈现在另一个标记文件的渲染输出中。
而不是单个视图文件,现在我们的catalog视图将由各种逻辑部分组成:
通过使用隔离的部分作为部分视图,每个文件现在具有比一体化视图文件更高的可维护性。
要将部分视图应用于我们的应用程序,首先我们将大部分标记内容提取到新的_Categories.cshtml文件。请注意_Categories.cshtml以下划线开头,这是部分视图的默认命名约定。
原始的Index.cshtml文件必须包含_Categories.cshtml标记的元素。标签实际上是一个标签辅助器(Microsoft.AspNetCore.Mvc.PartialTagHelper类),它在服务器上运行并在该位置呈现类别。
@{
ViewData["Title"] = "Catalog";
var products = Enumerable.Range(0, 30);
}
清单9:Catalog/Index.cshtml文件
除PartialTagHelper之外,还可以参考部分视图HtmlHelper。使用HtmlHelper的最佳做法是调用PartialAsync。在以下代码片段中,PartialAsync方法返回包含在Task
@{
ViewData["Title"] = "Catalog";
var products = Enumerable.Range(0, 30);
}
@await Html.PartialAsync("_Categories", products);
代码清单9.1:Catalog/Index.cshtml文件中的Html.PartialAsync变体
请注意,代码清单9.1中的代码产生与清单9中的代码完全相同的结果。另请注意我们如何将products模型作为方法的参数传递。
另一方面,_Categories.cshtml文件看起来与任何普通的Razor标记文件完全相同:我们可以定义@model 指令,HTML元素,标记辅助器,C#代码等。您也可以使用标记帮助程序包含内部部分视图,如下面的文件中所示:
@model IEnumerable;
@{
var products = Model;
const int productsPerCategory = 5;
const int PageSize = 4;
}
@for (int category = 0; category < (products.Count() / productsPerCategory); category++)
{
Category @(category + 1)
}
清单10:Catalog/_Categories.cshtml文件
现在,最后一个catalog部分视图必须是具有产品卡详细信息的视图。
@model int;
@{
var productIndex = Model;
}
清单11:Catalog/ _ProductCard.cshtml文件
请注意如何通过将产品代码与图像路径的其余部分连接起来,通过适当的路径提供产品图像URL。
Catalog index视图不仅用于显示,还用于搜索产品。上半部分将显示一个表单,用户将在其中输入并提交搜索文本,以便只有匹配的产品或类别名称才会显示在catalog中。
同样,我们应该在主Index.cshtml Razor文件中添加一个新的部分视图标记辅助器(
)。
@ {
var products = Enumerable.Range(0, 30);
}
清单12:Catalog/ _ProductCard.cshtml文件
请注意Index视图如何保持干净和简单。由于_SearchProducts部分视图不需要任何数据,因此不会向其传递任何参数。
_SearchProducts部分视图基本上是与将信息发送到服务器所需的一些元素(标签+文本字段+提交按钮)的表单。
Search products
清单13:Catalog/ _SearchProducts.cshtml文件
到目前为止,表单没有做任何事情。但我们将在下一篇文章中实现搜索功能。
用户选择任何产品后,必须将他/她重定向到“我的购物篮”视图。此视图负责购物车功能,并将保存订单商品信息列表,例如:
到目前为止,我们只有HomeController,我们的catalog Index()操作所在的位置。我们也可以使用HomeController 来保存Basket Index,但是不要让我们应用程序中唯一的控制器混乱,让我们为catalog保留一个控制器,为basket保留另一个控制器。
但由于“HomeController”并没有太多的内容,所以让它通过将其名称改为“CatalogController” 来使其更具描述性。这还需要我们将View/Home文件夹重命名为View/Catalog:
图10:将Home controller重命名为Catalog
而且,由于CatalogController还具有用于显示Error视图的一般操作,因此最好将该操作提取到父类,该类是CatalogController和BasketController都要继承的基类:
public abstract class BaseController : Controller
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
清单14:BaseController.cs文件
现在让我们让两个控制器继承这个基类:
public class CatalogController : BaseController
{
public IActionResult Index()
{
return View();
}
}
public class BasketController : BaseController
{
public IActionResult Index()
{
return View();
}
}
清单15:CatalogController和BasketController类
此时,如果您尝试再次执行应用程序,您会注意到应用程序崩溃的方式,因为它仍在寻找位于名为HomeController的控制器的Index操作。这称为我们使用MVC项目模板创建新项目时配置的“默认路由 ”。
现在,我们必须通过将默认控制器从“Home” 重命名为“Catalog”来更改默认路由:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Catalog}/{action=Index}/{id?}");
});
清单16:在Startup.cs中设置新的默认路由到CatalogController
至于Basket视图,我们再次使用Bootstrap组件来创建用户界面。它基本上是一个Bootstrap卡 组件,包括一个卡头,包含多个用于basket项名称的列标题,一个用于basket项详细信息的卡片主体,以及一个用于总计/项计数的卡片页脚。
图11:Bootstrap 4卡组件
我们可以看到,到目前为止篮子项数据只是在视图中声明的数组。稍后,此数据将替换为来自控制器的数据。
@{
ViewData["Title"] = "My Basket";
var items = new[]
{
new { Id = 1, ProductId = 1, Name = "Broccoli", UnitPrice = 59.90, Quantity = 2 },
new { Id = 2, ProductId = 5, Name = "Green Grapes", UnitPrice = 59.90, Quantity = 3 },
new { Id = 3, ProductId = 9, Name = "Tomato", UnitPrice = 59.90, Quantity = 4 }
};
}
My Basket
Item
Unit Price
Quantity
Subtotal
@foreach (var item in items)
{
@item.Name
@item.UnitPrice.ToString("C")
@((item.Quantity * item.UnitPrice).ToString("C"))
}
清单17:Catalog/Index.cshtml文件
作为最后一步,我们现在可以通过添加CSS规则来对齐basket项:
.row-center {
display: flex;
align-items: center;
}
清单18:site.css文件
请注意我们如何使用flexbox布局,与Bootstrap 4中使用的布局完全相同。
图12:basket清单
再一次,我们通过将其拆分为部分视图来分解大的Basket视图,就像我们使用Catalog标记一样。
在处理部分视图之前,让我们创建一个新类来保存basket项数据:
public class BasketItem
{
public int Id { get; set; }
public int ProductId { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
}
清单19:BasketController.cs中定义的模拟Basket项类
部分视图的优点之一是可重用性。我们的basket项有两个部分,一个在basket列表卡下面,另一个在basket列表卡下面,两个部分都有完全相同的控制按钮:
清单20:Basket/ _BasketControls.cshtml文件
我们可以看到,这些标记是重复的。幸运的是,部分视图允许我们避免这种重复。
主basket图现在看起来更简单,_BasketControls部分视图在basket列表部分视图的上方和下方都实现。
@using MVC.Controllers
@{
ViewData["Title"] = "My Basket";
List items = new List
{
new BasketItem { Id = 1, ProductId = 1, Name = "Broccoli",
UnitPrice = 59.90m, Quantity = 2 },
new BasketItem { Id = 2, ProductId = 5, Name = "Green Grapes",
UnitPrice = 59.90m, Quantity = 3 },
new BasketItem { Id = 3, ProductId = 9, Name = "Tomato",
UnitPrice = 59.90m, Quantity = 4 }
};
}
My Basket
清单21:Basket / Index.cshtml文件
这是提取到新的部分视图(_BasketList.cshtml)的basket列表标记:
@using MVC.Controllers
@model List;
@{
var items = Model;
}
Item
Unit Price
Quantity
Subtotal
@foreach (var item in items)
{
}
清单22:Basket/ _BasketList.cshtml文件
对于购物篮项详细信息,我们然后创建最后一个部分视图,_BasketItem.cshtml文件。注意如何通过将数量乘以单位价格来计算小计:
@using MVC.Controllers
@model BasketItem
@{
var item = Model;
}
@item.Name
@item.UnitPrice.ToString("C")
@((item.Quantity * item.UnitPrice).ToString("C"))
清单23:Basket/ _BasketItem.cshtml文件
在用户决定将哪些产品和数量包括在购物车中之后,用户可以选择继续完成订单。但首先,需要一些个人信息,这通常是典型电子商务程序所必需的,例如计费,发票和运输等。
图13:registration 视图
using Microsoft.AspNetCore.Mvc;
namespace MVC.Controllers
{
public class RegistrationController : BaseController
{
public IActionResult Index()
{
return View();
}
}
}
清单24:RegistrationController类
接下来,registration 视图必须包含收集个人信息所需的所有字段:
Registration
清单25:Registration/Index.cshtml文件
请注意我们如何再次省略表单操作,因为将来会提供数据库更新功能。
一旦客户填写了个人信息,我们就假设该过程一切正常,然后将他/她重定向到新的网页,通知我们的客户订单已经下达,并要求他/她等待进一步的指示直到订单被处理并生成。
就目前而言,Checkout控制器也是一个非常简单的类,与其他类似:
public class CheckoutController : BaseController
{
public IActionResult Index()
{
return View();
}
}
清单26:CheckoutController类
该视图只是几行标记,带有post-basket 指令的静态内容。这里唯一的动态信息是客户电子邮件地址。
@{
ViewData["Title"] = "Checkout";
var email = "[email protected]";
}
Order Has Been Placed!
Your order has been placed.
Soon you will receive an e-mail at @email including all order details.
清单27:Checkout/Index.cshtml文件
图14:Checkout视图
我们的申请流程要求在购物车结账时不立即处理订单,而是在未来的某个时间点异步处理。
public class NotificationsController : BaseController
{
public IActionResult Index()
{
return View();
}
}
清单28:NotificationsController类
随着客户不断购买,异步订购流程可能需要一些时间才能保存实际的数据库订单数据详细信息。因此,我们有一个通知视图,客户可以检查他/她以前的购买,从这一点,获取有关实际订单的更多信息,如开票,发货等。
@{
ViewData["Title"] = "Notifications";
}
User Notifications
Message
Date / Time
New order placed successfully: 2
13/04/2019
18:04
清单29:Notifications/Index.cshtml文件
图15:通知视图
到目前为止,我们有一个catalog,它不显示实际产品,而是显示模型数据。让我们开始一个新的重构周期,以便我们可以将更多真实数据注入catalog视图。
这种数据通常来自数据库或Web服务。但在我们的例子中,让我们通过阅读静态JSON文件来检索它们。该products.json文件被放置在我们的根项目文件夹,其内容是这样的:
[
{
"number": 1,
"name": "Oranges",
"category": "Fruits",
"price": 5.90
},
{
"number": 2,
"name": "Lemons",
"category": "Fruits",
"price": 5.90
},
.
.
.
]
清单30:products.json文件
在现实世界的场景中,我们的catalog数据库最初将使用此JSON文件数据进行填充。这个过程叫做“seeding”。我们将使用JSON文件“seeding”数据库。但由于我们还没有数据库,我们将使用种子数据作为catalog视图的直接来源。
我们对“MVC”中的“M”部分仍然没有做太多。对于模型,我们创建了两个类:Product和Category。由于这两个类都具有Id属性,因此我们可以将其移动到超类以由模型类继承。
using System.Runtime.Serialization;
namespace MVC.Models
{
public abstract class BaseModel
{
public int Id { get; set; }
}
}
清单31:BaseModel类
public class Category : BaseModel
{
public Category(int id, string name)
{
Id = id;
Name = name;
}
public string Name { get; private set; }
}
清单32:Category类
对于Product类,我们可以提供一个新的只读ImageURL属性来计算图像路径。这将剥夺构建路径的责任。
public class Product : BaseModel
{
public Category Category { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string ImageURL { get { return $"/images/catalog/large_{Code}.jpg"; } }
public Product(int id, string code, string name, decimal price, Category category)
{
Id = id;
Code = code;
Name = name;
Price = price;
Category = category;
}
}
清单33:Product类
以下类负责读取products.json文件,将其反序列化为product对象集合,然后返回product列表。
public class SeedData
{
public static async Task> GetProducts()
{
var json = await File.ReadAllTextAsync("products.json");
var data = JsonConvert.DeserializeObject>(json);
var dict = new Dictionary();
var categories =
data
.Select(i => i.category)
.Distinct();
foreach (var name in categories)
{
var category = new Category(dict.Count + 1, name);
dict.Add(name, category);
}
var products = new List();
foreach (var item in data)
{
Product product = new Product(
products.Count + 1,
item.number.ToString("000"),
item.name,
item.price,
dict[item.category]);
products.Add(product);
}
return products;
}
}
public class ProductData
{
public int number { get; set; }
public string name { get; set; }
public string category { get; set; }
public decimal price { get; set; }
}
清单34:SeedData类
但是,当然,我们也有一些代码需要重构。要修改的第一个组件是catalog控制器。
我们将产品列表加载到部分变量中,然后将其作为模型参数传递给View。
public class CatalogController : BaseController
{
public async Task Index()
{
var products = await SeedData.GetProducts();
return View(products);
}
}
清单35:CatalogController类
此外,必须在catalog Index视图中修改模型为List类型。
@model List;
@using MVC.Models;
@{
ViewData["Title"] = "Catalog";
}
清单36:Index.cshtml
现在我们必须用带有模型数据的C#表达式替换产品字段:
@model Product;
@using MVC.Models;
@{
var product = Model;
}