ASP.NET核心之路微服务第01部分:构建视图
目录
介绍
软件要求
创建项目
Index页面
样式
品牌
部分视图
搜索产品部分视图
Basket视图
Basket部分视图
登记视图
结帐视图
通知视图
JSON产品加载
应用导航
结论
- 下载第1部分来源 - 2.9 MB
介绍
在本文中,我将展示如何在Visual Studio的帮助下使用ASP.NET Core创建基本的电子商务应用程序流。
本文是演示可在ASP.NET核心电子商务应用程序中实现的各种实践,模式,技术和框架的系列文章中的第一篇,同时我们逐步实现构建微服务解决方案的最终目标。
也就是说,如果您对本文中没有找到任何真正的微服务感到有点失望,那是因为我想要涵盖的主题太多了,仅仅在一篇文章中就不可能讨论所有这些问题(或者讨论它们,而不仅仅是表面上的问题)。此外,我不会立即提供微服务,因为我希望以“渐进式”逐步方法工作,重构和推进代码库,因为我们将继续前进。请耐心等待,享受骑行乐趣。
由于这只是整个系列的第一篇文章,我想列举下一部分的设想主题:
- 第1部分:构建视图
- 第2部分:查看组件
- 第3部分:ASP.NET Core Identity
- 第4部分:SQLite
- 第5部分:Dapper
- 第6部分:SignalR
- 第7部分:单元测试Web API
- 第8部分:单元测试Web MVC应用程序
- 第9部分:监测健康检查
- 第10部分:Redis数据库
- 第11部分:IdentityServer4
- 第12部分:订购Web API
- 第13部分:Basket Web API
- 第14部分:Catalog Web API
- 第15部分:具有Polly的弹性HTTP客户端
- 第16部分:使用Swagger记录Web API
- 第17部分:Docker容器
- 第18部分:Docker配置
- 第19部分:使用Kibana进行中心日志
我们可以看到,有很多主题要涵盖。虽然部件已编号,但这仅用于计数目的。事实上,实际的顺序可以随着我们的进步而改变。
软件要求
- 下载并安装Visual Studio社区或高级版。
创建项目
该项目将使用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<Startup>();
}
清单1:Program类
正如我们在这里看到的那样,调用WebHost.CreateDefaultBuilder()方法以便可以创建webhost,但由于需要配置它,我们还必须调用UseStartup()以传递负责webhost配置的Startup 类名。让我们看看这个类是如何工作的以及它将如何在我们的应用程序中使用。
Startup结构简单。它只包含两种方法:
- ConfigureServices()
- Configure()
在此上下文中,“服务”是可以添加的任何组件,以便为我们的应用程序提供特定功能,例如:MVC,日志记录,数据库,身份验证,cookie,会话等。
这些组件也称为“中间件”,可以是“管道”的一部分。每个中间件决定是否将请求传递给管道中的下一个组件,并且可以包括要在管道中的后续组件之前或之后执行的算法。
通常,名为“MyService” 的服务将在我们的Startup类中引用两次:
- 首先,在AddMyService()方法的ConfigureServices()方法中。这里,该AddMyService()方法将提供适当的配置,以便服务可以正常运行;
- 然后,在UseMyService()在Configure()方法。
我们来看看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<CookiePolicyOptions>(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()方法
在这里,我们对要添加到请求管道的每个服务进行简短描述:
- app.UseDeveloperExceptionPage:操作完成后对应用程序的引用
- app.UseExceptionHandler:向管道添加中间件,以捕获异常,记录它们,重置请求路径,然后重新执行请求
- app.UseHsts:为当前请求路径启用静态文件服务
- app.UseHttpsRedirection:将CookiePolicyMiddleware处理程序添加到指定的IApplicationBuilder,从而启用cookie策略功能
- app.UseStaticFiles:为当前请求路径启用静态文件服务
- app.UseCookiePolicy:将CookiePolicyMiddleware处理程序添加到指定的IApplicationBuilder,从而启用cookie策略功能
- app.UseMvc:将MVC添加到IApplicationBuilder请求执行管道
Index页面
我们将在我们的商店中使用有限的30种产品。对于每一个,我们都有一个图像,要添加到wwwroot项目文件夹内的 /images/catalog文件夹中。
图5:产品catalog图像
这些产品将在主页内的“catalog”视图中显示。此catalog显示为按每个产品类别分组的一组产品。每个类别都有一个名为“Carousel” 的Bootstrap 4组件,它可以按4个产品组自动旋转category产品。
@{ViewData["Title"] = "Home Page";
}@for (int category = 0; category < 6; category++)
{<h3>Category Name</h3><div id="carouselExampleIndicators-@category" class="carousel slide" data-ride="carousel"><ol class="carousel-indicators"><li data-target="#carouselExampleIndicators-@category" data-slide-to="0" class="active"></li><li data-target="#carouselExampleIndicators-@category" data-slide-to="1"></li><li data-target="#carouselExampleIndicators-@category" data-slide-to="2"></li></ol><div class="carousel-inner"><div class="carousel-item active"><div class="container"><div class="row">@for (int i = 0; i < 4; i++){<div class="col-sm-3"><div class="card"><div class="card-body"><img class="d-block w-100" src="~/images/catalog/large_@((i+1 + category * 5).ToString("000")).jpg"></div><div class="card-footer"><p class="card-text">Product Name</p><h5 class="card-title text-center">$ 39.90</h5><div class="text-center"><a href="#" class="btn btn-success">Add to basket</a></div></div></div></div>}</div></div></div><div class="carousel-item"><div class="container"><div class="row">@for (int i = 0; i < 1; i++){<div class="col-sm-3"><div class="card"><div class="card-body"><img class="d-block w-100" src="~/images/catalog/large_@((i+5 + category * 5).ToString("000")).jpg"></div><div class="card-footer"><p class="card-text">Product Name</p><h5 class="card-title text-center">$ 39.90</h5><div class="text-center"><a href="#" class="btn btn-success">Add to basket</a></div></div></div></div>}</div></div></div></div><a class="carousel-control-prev" href="#carouselExampleIndicators-@category" role="button" data-slide="prev"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="sr-only">Previous</span></a><a class="carousel-control-next" href="#carouselExampleIndicators-@category" role="button" data-slide="next"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="sr-only">Next</span></a></div>
}
清单4:catalogIndex视图
样式
这是一小步,但由于Bootstrap 4不再带有icon fonts (gliphicon),所以我们自己安装它。
Visual Studio允许我们安装客户端库,例如Font Awesome,一种流行的图标字体包。
图6:添加客户端库菜单
图7:添加客户端库对话框
既然已经安装了字体文件,我们必须在_Layout.cshtml文件中引用它们:
<link href="~/lib/font-awesome/css/font-awesome.css" rel="stylesheet" />
<link rel="stylesheet" href="~/css/site.css" />
清单5:添加Font Awesome引用到_Layout.cshtml文件
让我们看看如何添加我们的第一个图标。在Home/Index.cshtml视图中,我们添加了一个带有fa fa-shopping-cart类的HTML元素。
<a href="#" class="btn btn-success"><span class="fa fa-shopping-cart"></span>Add to basket
</a>
清单6:在catalog的Index视图中使用Font Awesome图标
这将自动显示“添加到购物篮”按钮左侧的购物车图标。
再次运行应用程序,我们看到如何呈现购物车图标:
图8:Font Awesome icons
品牌
通过打开_Layout.cshtml文件,我们可以使用我们公司的名称更改品牌。
<div class="container">© 2019 - The Grocery Store - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
清单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视图将由各种逻辑部分组成:
- Views/Catalog
- Index.cshtml
- _SearchProducts.cshtml
- _Categories.cshtml
- _ProductCard.cshtml
通过使用隔离的部分作为部分视图,每个文件现在具有比一体化视图文件更高的可维护性。
要将部分视图应用于我们的应用程序,首先我们将大部分标记内容提取到新的_Categories.cshtml文件。请注意_Categories.cshtml以下划线开头,这是部分视图的默认命名约定。
原始的Index.cshtml文件必须包含_Categories.cshtml标记的元素。标签实际上是一个标签辅助器(Microsoft.AspNetCore.Mvc.PartialTagHelper类),它在服务器上运行并在该位置呈现类别。
@{ViewData["Title"] = "Catalog";var products = Enumerable.Range(0, 30);
}<partial name="_Categories" for="@products" />
清单9:Catalog/Index.cshtml文件
除PartialTagHelper之外,还可以参考部分视图HtmlHelper。使用HtmlHelper的最佳做法是调用PartialAsync。在以下代码片段中,PartialAsync方法返回包含在Task<TResult>中的IHtmlContent类型。通过在等待的调用前添加一个@字符来引用该方法,以指示Razor引擎这是一个C#代码:
@{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<int>;@{var products = Model;const int productsPerCategory = 5;const int PageSize = 4;
}@for (int category = 0; category < (products.Count() / productsPerCategory); category++)
{<h3>Category @(category + 1)</h3><div id="carouselExampleIndicators-@category" class="carousel slide" data-ride="carousel"><div class="carousel-inner">@{int pageCount = (int)Math.Ceiling((double)productsPerCategory / PageSize);var productsInCategory =products.Skip(category * productsPerCategory).Take(productsPerCategory);for (int pageIndex = 0; pageIndex < pageCount; pageIndex++){<div class="carousel-item @(pageIndex == 0 ? "active" : "")"><div class="container"><div class="row">@{var productsInPage =productsInCategory.Skip(pageIndex * PageSize).Take(PageSize);foreach (var productIndex in productsInPage){<partial name="_ProductCard" for="@productIndex"/>}}</div></div></div>}}</div><a class="carousel-control-prev" href="#carouselExampleIndicators-@category" role="button" data-slide="prev"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="sr-only">Previous</span></a><a class="carousel-control-next" href="#carouselExampleIndicators-@category" role="button" data-slide="next"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="sr-only">Next</span></a></div>
}
清单10:Catalog/_Categories.cshtml文件
现在,最后一个catalog部分视图必须是具有产品卡详细信息的视图。
@model int;@{ var productIndex = Model;
}<div class="col-sm-3"><div class="card"><div class="card-body"><img class="d-block w-100" src="~/images/catalog/large_@((productIndex + 1).ToString("000")).jpg"></div><div class="card-footer"><p class="card-text">Product Name</p><h5 class="card-title text-center">$ 39.90</h5><a href="#" class="btn btn-success"><span class="fa fa-shopping-cart"></span>Add to basket</a></div></div></div>
</div>
清单11:Catalog/ _ProductCard.cshtml文件
请注意如何通过将产品代码与图像路径的其余部分连接起来,通过适当的路径提供产品图像URL。
搜索产品部分视图
Catalog index视图不仅用于显示,还用于搜索产品。上半部分将显示一个表单,用户将在其中输入并提交搜索文本,以便只有匹配的产品或类别名称才会显示在catalog中。
同样,我们应该在主Index.cshtml Razor文件中添加一个新的部分视图标记辅助器(<partial>
)。
@ {var products = Enumerable.Range(0, 30);
}<partial name="_SearchProducts"/><partial name="_Categories" for="@products" />
清单12:Catalog/ _ProductCard.cshtml文件
请注意Index视图如何保持干净和简单。由于_SearchProducts部分视图不需要任何数据,因此不会向其传递任何参数。
_SearchProducts部分视图基本上是与将信息发送到服务器所需的一些元素(标签+文本字段+提交按钮)的表单。
<div class="container"><h2>Search products</h2><div id="custom-search-input"><div class="input-group col-md-12"><form><div class="container"><div class="row"><div><input type="text" name="search"class="form-control input-lg"placeholder="category or product" /></div><div><div class="input-group-btn text-center"><a href="#" class="btn btn-success"><span class="fa fa-search"></span></a></div></div></div></div></form></div></div>
</div>
清单13:Catalog/ _SearchProducts.cshtml文件
到目前为止,表单没有做任何事情。但我们将在下一篇文章中实现搜索功能。
Basket视图
用户选择任何产品后,必须将他/她重定向到“我的购物篮”视图。此视图负责购物车功能,并将保存订单商品信息列表,例如:
- 产品图象
- 产品名称
- 物品数量
- 单价
- 小计
到目前为止,我们只有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 }};
}<div class="row"><div class="col-sm-12"><div class="pull-right"><a class="btn btn-success" href="/">Add More Products</a><a class="btn btn-success" href="/registration">Fill in Registration</a></div></div>
</div><h3>My Basket</h3><div class="card"><div class="card-header"><div class="row"><div class="col-sm-6">Item</div><div class="col-sm-2 text-center">Unit Price</div><div class="col-sm-2 text-center">Quantity</div><div class="col-sm-2"><span class="pull-right">Subtotal</span></div></div></div><div class="card-body">@foreach (var item in items){<div class="row row-center"><div class="col-sm-2"><img class="img-product-basket w-75" src="/images/catalog/large_@(item.ProductId.ToString("000")).jpg" /></div><input type="hidden" name="productId" value="012" /><div class="col-sm-4">@item.Name</div><div class="col-sm-2 text-center">@item.UnitPrice.ToString("C")</div><div class="col-sm-2 text-center"><div class="input-group"><button type="button" class="btn btn-light"><span class="fa fa-minus"></span></button><input type="text" value="@item.Quantity"class="form-control text-center quantity" /><button type="button" class="btn btn-light"><span class="fa fa-plus"></span></button></div></div><div class="col-sm-2"><div class="pull-right"><span class="pull-right" subtotal>@((item.Quantity * item.UnitPrice).ToString("C"))</span></div></div></div><br />}</div><div class="card-footer"><div class="row"><div class="col-sm-10"><span numero-items>Total: @items.Lengthitem@(items.Length > 1 ? "s" : "")</span></div><div class="col-sm-2">Total: <span class="pull-right" total>@(items.Sum(item => item.Quantity * item.UnitPrice).ToString("C"))</span></div></div></div>
</div><br /><div class="row"><div class="col-sm-12"><div class="pull-right"><a class="btn btn-success" href="/">Add More Products</a><a class="btn btn-success" href="/registration">Fill in Registration</a></div></div>
</div>
清单17:Catalog/Index.cshtml文件
作为最后一步,我们现在可以通过添加CSS规则来对齐basket项:
.row-center {display: flex;align-items: center;
}
清单18:site.css文件
请注意我们如何使用flexbox布局,与Bootstrap 4中使用的布局完全相同。
图12:basket清单
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列表卡下面,两个部分都有完全相同的控制按钮:
- 添加更多产品
- 填写登记表
<div class="row"><div class="col-sm-12"><div class="pull-right"><a class="btn btn-success" href="/">Add More Products</a><a class="btn btn-success" href="/">Fill in Registration</a></div></div>
</div>
清单20:Basket/ _BasketControls.cshtml文件
我们可以看到,这些标记是重复的。幸运的是,部分视图允许我们避免这种重复。
主basket图现在看起来更简单,_BasketControls部分视图在basket列表部分视图的上方和下方都实现。
@using MVC.Controllers
@{ViewData["Title"] = "My Basket";List<BasketItem> items = new List<BasketItem>{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 }};
}<partial name="_BasketControls" /><h3>My Basket</h3><partial name="_BasketList" for="@items" />
<br />
<partial name="_BasketControls" />
清单21:Basket / Index.cshtml文件
这是提取到新的部分视图(_BasketList.cshtml)的basket列表标记:
@using MVC.Controllers
@model List<BasketItem>;@{var items = Model;
}<div class="card"><div class="card-header"><div class="row"><div class="col-sm-6">Item</div><div class="col-sm-2 text-center">Unit Price</div><div class="col-sm-2 text-center">Quantity</div><div class="col-sm-2"><span class="pull-right">Subtotal</span></div></div></div><div class="card-body">@foreach (var item in items){<partial name="_BasketItem" for="@item" />}</div><div class="card-footer"><div class="row"><div class="col-sm-10"><span numero-items>Total: @items.Countitem@(items.Count > 1 ? "s" : "")</span></div><div class="col-sm-2">Total: <span class="pull-right" total>@(items.Sum(item => item.Quantity* item.UnitPrice).ToString("C"))</span></div></div></div>
</div>
清单22:Basket/ _BasketList.cshtml文件
对于购物篮项详细信息,我们然后创建最后一个部分视图,_BasketItem.cshtml文件。注意如何通过将数量乘以单位价格来计算小计:
@using MVC.Controllers@model BasketItem@{var item = Model;
}<div class="row row-center product-line" item-id="@item.Id.ToString("000")"><div class="col-sm-2"><img class="img-product-basket w-75" src="/images/catalog/large_@(item.ProductId.ToString("000")).jpg" /></div><input type="hidden" name="productId" value="012" /><div class="col-sm-4">@item.Name</div><div class="col-sm-2 text-center">@item.UnitPrice.ToString("C")</div><div class="col-sm-2 text-center"><div class="input-group"><button type="button" class="btn btn-light"><span class="fa fa-minus"></span></button><input type="text" value="@item.Quantity"class="form-control text-center quantity" /><button type="button" class="btn btn-light"><span class="fa fa-plus"></span></button></div></div><div class="col-sm-2"><div class="pull-right"><span class="pull-right" subtotal>@((item.Quantity * item.UnitPrice).ToString("C"))</span></div></div>
</div>
<br />
清单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 视图必须包含收集个人信息所需的所有字段:
<h3>Registration</h3><form method="post" action="/"><div class="card"><div class="card-body"><div class="row"><div class="col-sm-4"><div class="form-group"><label class="control-label">Customer Name</label><input type="text" class="form-control" /></div><div class="form-group"><label class="control-label">Email</label><input type="email" class="form-control" /></div><div class="form-group"><label class="control-label">Phone</label><input type="text" class="form-control" /></div></div><div class="col-sm-4"><div class="form-group"><label class="control-label">Address</label><input type="text" class="form-control" /></div><div class="form-group"><label class="control-label">Additional Address</label><input type="text" class="form-control" /></div><div class="form-group"><label class="control-label">District</label><input type="text" class="form-control" /></div></div><div class="col-sm-4"><div class="form-group"><label class="control-label">City</label><input type="text" class="form-control" /></div><div class="form-group"><label class="control-label">State</label><input type="text" class="form-control" /></div><div class="form-group"><label class="control-label">Zip Code</label><input type="text" class="form-control" /></div><div class="form-group"><a class="btn btn-success" href="/">Keep buying</a></div><div class="form-group"><button type="submit"class="btn btn-success button-notification">Check out</button></div></div></div></div></div>
</form>
清单25:Registration/Index.cshtml文件
请注意我们如何再次省略表单操作,因为将来会提供数据库更新功能。
结帐视图
一旦客户填写了个人信息,我们就假设该过程一切正常,然后将他/她重定向到新的网页,通知我们的客户订单已经下达,并要求他/她等待进一步的指示直到订单被处理并生成。
就目前而言,Checkout控制器也是一个非常简单的类,与其他类似:
public class CheckoutController : BaseController
{public IActionResult Index(){return View();}
}
清单26:CheckoutController类
该视图只是几行标记,带有post-basket 指令的静态内容。这里唯一的动态信息是客户电子邮件地址。
@{ ViewData["Title"] = "Checkout";var email = "alice@smith.com";
}
<h3>Order Has Been Placed!</h3><div class="panel-info"><p>Your order has been placed.</p><p>Soon you will receive an e-mail at <b>@email</b> including all order details.</p><p><a href="/" class="btn btn-success">Back to product catalog</a></p>
</div>
清单27:Checkout/Index.cshtml文件
图14:Checkout视图
我们的申请流程要求在购物车结账时不立即处理订单,而是在未来的某个时间点异步处理。
通知视图
public class NotificationsController : BaseController
{public IActionResult Index(){return View();}
}
清单28:NotificationsController类
随着客户不断购买,异步订购流程可能需要一些时间才能保存实际的数据库订单数据详细信息。因此,我们有一个通知视图,客户可以检查他/她以前的购买,从这一点,获取有关实际订单的更多信息,如开票,发货等。
@{ViewData["Title"] = "Notifications";
}
<h3>User Notifications</h3><div class="row"><div class="col-sm-12"><div class="pull-right"><a class="btn btn-success" href="/">Back to Catalog</a></div></div>
</div>
<br /><div class="card"><div class="card-header"><div class="row"><div class="col-sm-2 text-center"><!--NEW?--></div><div class="col-sm-8">Message</div><div class="col-sm-2 text-center">Date / Time</div></div></div><div class="card-body notifications"><div class="row"><div class="col-sm-2 text-center"><span class="fa fa-envelope-open"></span></div><div class="col-sm-8">New order placed successfully: 2</div><div class="col-sm-2 text-center"><span>13/04/2019</span> <span>18:04</span></div></div></div>
</div>
<br />
<div class="row"><div class="col-sm-12"><div class="pull-right"><a class="btn btn-success" href="/">Back to Catalog</a></div></div>
</div>
清单29:Notifications/Index.cshtml文件
图15:通知视图
JSON产品加载
到目前为止,我们有一个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<List<Product>> GetProducts(){var json = await File.ReadAllTextAsync("products.json");var data = JsonConvert.DeserializeObject<List<ProductData>>(json);var dict = new Dictionary<string, Category>();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<Product>();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<IActionResult> Index(){var products = await SeedData.GetProducts();return View(products);}
}
清单35:CatalogController类
此外,必须在catalog Index视图中修改模型为List类型。
@model List<Product>;
@using MVC.Models;
@{ViewData["Title"] = "Catalog";
}<partial name="_SearchProducts"/><partial name="_Categories" for="@Model" />
清单36:Index.cshtml
现在我们必须用带有模型数据的C#表达式替换产品字段:
- @(product.ImageURL)
- @product.Name
- @product.Price.ToString("C")
@model Product;
@using MVC.Models;@{ var product = Model;
}<div class="col-sm-3"><div class="card"><div class="card-body"><img class="d-block w-100" src="@(product.ImageURL)"></div><div class="card-footer"><p class="card-text">@product.Name</p><h5 class="card-title text-center">@product.Price.ToString("C")</h5><div class="text-center"><a href="#" class="btn btn-success"><span class="fa fa-shopping-cart"></span>
.
.
.
清单37:Catalog / _ProductCard.cshtml文件
此外,_Categories部分视图将被重构。首先,我们将模型类型更改为List,并将类别变量赋值更改为LINQ查询,该查询仅为我们提供product列表中的不同类别对象。
@model List<Product>;@{var products = Model;const int PageSize = 4;var categories = products.Select(p => p.Category).Distinct();
}
.
.
.
@foreach (var category in categories)
{<h3>@category.Name</h3><div id="carouselExampleIndicators-@category.Id" class="carousel slide" data-ride="carousel">
.
.
.var productsInCategory = products.Where(p => p.Category.Id == category.Id);int pageCount = (int)Math.Ceiling((double)productsInCategory.Count() / PageSize);
.
.
.
<a class="carousel-control-prev" href="#carouselExampleIndicators-@category.Id" role="button" data-slide="prev">
.
.
.
<a class="carousel-control-next" href="#carouselExampleIndicators-@category.Id" role="button" data-slide="next">
清单38:Catalog/_Categories.cshtml文件
由于我们使用不同的Bootstrap 4 Carousel组件,因此必须通过category id属性(@category.Id)来标识它们。
现在,productsInCategory本地变量包含每个类别中的产品集合,我们将这些产品分组,以便可以适当地填充每个轮播。
应用导航
到目前为止,每个视图仍然是孤立的,并且没有链接将视图相互连接。让我们使用AnchorTagHelper提供导航以生成正确的链接。
虽然HTML标签具有相同的外观,但事实上AnchorTagHelper在服务器端运行,它根据以下属性计算锚点URL:
- asp-controller:MVC控制器名称。省略时,将假定当前控制器。
- asp-action:路径名称。省略时,将假定默认操作(Index)。
- asp-route-*:动作参数。每个动作参数必须单独提供。
第一个链接将从catalog视图到购物篮(basket)列表。每次客户选择产品时,必须显示购物车,显示所选项目,并显示一个数量。
我们如何将普通的HTML锚点元素更改为AnchorTagHelper?
首先,我们采用当前的锚点元素......
<a href="#" class="btn btn-success">
清单39:标记帮助器之前的Anchor元素
并替换href属性添加新asp-controller属性:
<a asp-controller="basket" class="btn btn-success">
清单40:标记助手之后的锚元素
源代码中的这一小变化产生了很大的影响:当ASP.NET Core使用Razor SDK编译视图时,这将注意到asp-controller属性,因此新链接将不再作为HTML锚点元素处理。相反,就像任何其他标记帮助器一样,它现在是一个服务器端组件,它在服务器上运行并呈现实际的HTML链接:
<a class="btn btn-success" href="/Basket">
清单41:由服务器端的标记帮助器呈现的锚点元素
现在让我们也应用AnchorTagHelper于basket 控件部分视图:
.
.
.<div class="pull-right"><a asp-controller="catalog" class="btn btn-success">Add More Products</a><a asp-controller="registration" class="btn btn-success">Fill in Registration</a></div>
清单42:Basket/_BasketControls.cshtml文件
结论
因此结束了文章系列的第一部分。如果你读到了这里,非常感谢你的耐心等待。
我们已经了解了如何使用Visual Studio创建新的ASP.NET Core项目,使用Razor引擎开发基本视图,为视图提供基本模型,并使用锚点标记助手将它们链接在一起。我们将使用相同的项目作为下一篇文章的起点,我们将在其中处理View Components。
原文地址:https://www.codeproject.com/Articles/3132654/ASP-NET-Core-Road-to-Microservices-Part-01-Buildin
ASP.NET核心之路微服务第01部分:构建视图相关推荐
- ASP.NET核心之路微服务第03部分:Identity
目录 介绍 文章系列 安装ASP.NET Core Identity 配置ASP.NET Core Identity 管理用户数据 使用Microsoft帐户,Google,Facebook等登录 结 ...
- ASP.NET核心之路微服务第02部分:查看组件
目录 文章系列 介绍 部分视图vs. 视图组件 用视图组件替换购物篮(Basket) 部分视图 用视图组件替换Catalog部分视图 用户通知计数器 结论 下载Part02.zip 文章系列 AS ...
- 【从零开始学微服务】01.微服务的过去与现在
大家好,欢迎来到万猫学社,跟我一起学,你也能成为微服务专家. 微服务的历史 再介绍什么是微服务之前,我们先了解一下微服务架构的历史,也就是微服务是如何提出来的. 2011年5月 2011年5月,在威尼 ...
- 若依微服务版的快速构建
若依微服务版的快速构建 目录 若依微服务版的快速构建 一.若依框架的介绍和所需要环境 系统需求 技术选型 内置功能 前期准备 二.下载若依微服务版本的代码 三.实现 初始化数据库 配置nacos 修改 ...
- Spring Cloud OAuth2 JWT 微服务认证服务器得构建
文章目录 Spring Cloud OAuth2 JWT 微服务认证服务器得构建 前言 认证服务得搭建 `AuthorizationServer` `WebSecurityConfig` `Autho ...
- 微服务下使用GraphQL构建BFF
https://zhuanlan.zhihu.com/p/35108457 微服务架构,这个在几年前还算比较前卫的技术在如今遍地开花.得益于开源社区的支持,我们可以轻松地利用 Spring Cloud ...
- Asp.Net Core Ocelot Consul 微服务
做一个简单的微服务架构如下图: 这个图表示的是一个网关代理Consul的两个服务,consul每个服务注册集群 安装 Consul的服务,这里安装单机版的,集群版配置最低要求(3个Consul ser ...
- Docker ASP.NET Core 2.0 微服务跨平台实践
本篇博文的目的:在 Mac OS 中使用 VS Code 开发 ASP.NET Core 2.0 应用程序,然后在 Ubuntu 服务器配置 Docker 环境,并使用 Docker 运行 Consu ...
- Docker Consul Fabio ASP.NET Core 2.0 微服务跨平台实践
相关博文: Ubuntu 简单安装 Docker Mac OS.Ubuntu 安装及使用 Consul Consul 服务注册与服务发现 Fabio 安装和简单使用 阅读目录: Docker 运行 C ...
最新文章
- 张继平院士:40年北大数学路 | 北大黄金一代是如何培养的
- 图形卷积神经网络有多强大?一文让你熟练掌握GCN
- 2021年春季学期-信号与系统-第十次作业参考答案-第四小题
- 屏蔽控制台应用程序的窗口#pragma comment(linker, /subsystem:windows /ENTRY:mainCRTStartup)...
- ASP.NET MVC 音乐商店 - 7.成员管理和授权
- 玩转StyleGAN2模型:教你生成动漫人物
- DM***+OSPF测试
- 鸿蒙os事例代码,鸿蒙HarmonyOS App开发造轮子之自定义圆形图片组件的实例代码
- TableView Within Alert
- np.linspace函数用法
- 第二次打卡 数据处理
- 开机加速与蓝屏stop:0x000000074
- Excel 调用百度翻译API进行翻译
- CUDA的Occupancy和Achieved Occupancy概念
- python中PIL的安装参考教程
- 二叉排序树的查找、插入、创建和删除
- 一号店首页代码需其他的联系我
- 联合办公空间该如何继续发展?
- Hive知识点总结(简明版)
- 国产LINUX服务器操作系统openEuler下的轻量开源虚拟化工具stratovirt
热门文章
- new to python什么意思_Python中__new__的作用
- cm如何查看各个组件版本_Oracle HowTo:查询Oracle各组件的版本信息
- python迭代函数例题_python map 函数使用,遍历访问可迭代对象
- html输入框间隔,input间的间距和文字上下居中
- 优秀的电商精品素材就到优图
- python add_subplot_Python使用add_subplot与subplot画子图操作
- std::deque简单使用
- DLL注入(CreateRemoteThread方式)
- NVIDIA Tesla K40C 的各项性能参数
- CentOS RPM源镜像源(国内+国外)