十五、TagHelpers
和ViewComponents
在本章中,我们将介绍:
- 使用环境、脚本和链接标签助手
- 使用表单 TagHelpers
- 以编程方式创建 TagHelpers
- 创建一个可重用的视图组件
- 创建一个视图组件/控制器类
使用环境、脚本和链接标签助手
在这个食谱中,我们将学习如何使用环境、脚本和链接标签助手。
准备
我们将使用以下 TagHelpers:
- Environment HTML 标签
- 在现有的 Script HTML 标签中,一些 TagHelper 属性:
asp-append-version
asp-src-include
asp-fallback-test
asp-fallback-src
asp-fallback-src-include
- 在现有的 Link HTML 标签中,一些 TagHelper 属性:
asp-append-version
asp-href-include
asp-fallback-test-class
asp-fallback-test-property
asp-fallback-test-value
asp-fallback-href
怎么做……
我们将通过创建一个在每个环境中行为不同的项目来深入使用 TagHelpers:Development
,Staging
和Production
:
- 首先,让我们使用 Environment TagHelper:
<environment names="Development">
<link rel="stylesheet"
href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href=https://ajax.aspnetcdn.com/ajax/
bootstrap/3.3.6/css/bootstrap.min.css />
</environment>
Environment TagHelper 被映射为应用属性中定义的环境变量ASPNETCORE_ENVIRONMENT
。 该变量在Startup.cs
中的应用配置中作为IHostingEnvironment
类型使用。
环境标签助手通常包含 Script 和 Link HTML 标签。
这种机制帮助我们用Startup.cs
中的 c#和_Layout.cshtml
文件中的 HTML 创建条件代码,以便管理不同的环境,例如生产环境、登台环境和开发环境。
通过右键单击项目的根目录并选择 Debug 部分,我们可以访问环境变量定义:
- 现在,要启用这种机制,我们必须在
Startup.cs
构造函数中为ConfigurationBuilder
对象执行.AddEnvironment()
方法。 通过这样做,我们通过依赖注入将环境变量注入到应用中:
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json",
optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
- 现在我们可以在代码中使用
IHostingEnvironment
类型来使用托管条件:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
…
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
…
}
我们还可以通过 Environment TagHelper 在 HTML 代码中声明式地管理条件:
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.min.js"></script>
</environment>
- 现在,让我们看看 Script HTML 标签中的 TagHelper 属性:
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
如果来自 CDN 的src
属性的值失败,则必须使用asp-fallback-src
的值。
asp-fallback-test
表示在使用来自 CDN 的脚本之前,必须测试其值(JavaScript 代码)。
asp-src-include
和asp-src-exclude
用于在视图中包含或排除 JavaScript 文件。
asp-fallback-src-include
和asp-fallback-src-exclude
用于在视图中包含或排除 JavaScript 文件,如果 JavaScript 加载的文件存在问题,并且asp-fallback-test
中的脚本已经过测试且未获批准。
asp-append-version
的值(true
或false
)决定是否刷新缓存,如果 CDN 文件的当前版本我们存储在浏览器的缓存已经更新自上次上传(如果没有,不需要再下载一次,如果是这样,也许我们想重新下载)。 这种机制称为缓存破坏。
- 最后,让我们看看 Link HTML 标签中的 TagHelper 属性:
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/
bootstrap/3.3.6/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only"
asp-fallback-test-property="position"
asp-fallback-test-value="absolute"
asp-href-include="~/css/plugins/jasny-bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.min.css"
asp-append-version="true" />
如果来自 CDN 的 href 属性的值失败,则必须使用asp-fallback-href
的值。
asp-fallback-test-class
、asp-fallback-test-property
和asp-fallback-test-value
表示对 CSS 类、CSS 属性及其值进行测试,以确保 CDN 下载正确。
asp-href-include
用于在视图中包含 CSS 文件。
asp-fallback-href-include
用于在加载 CND 文件时出现问题时在视图中包含 CSS 文件。
生成的代码如下:
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/plugins/jasny-bootstrap.min.css" />
<meta name="x-stylesheet-fallback-test" content="" class="sr-only" />
<script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT"),g=f[f.length-1].previousElementSibling,h=e.defaultView&&e.defaultView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;if(h&&h[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("position","absolute",["/lib/bootstrap/dist/css/bootstrap.min.css"]);</script>
<link rel="stylesheet" href="/css/site.min.css?v=SZ56l9iAMjwsC3lg_8ONpBnEfYbGculXCgb-yhj7aKs" />
使用表单 TagHelpers
在本食谱中,我们将学习如何使用 Form TagHelpers。
准备
我们将使用以下 TagHelpers:
- 在现有的 Form HTML 标记中,一些 TagHelper 属性:
asp-controller
asp-action
asp-route
asp-area
asp-antiforgery
- 在任何现有的 Form 元素中,一些 TagHelper 属性:
asp-for
asp-items
- 与任何现有的 Form 元素相关联的一些 TagHelper 属性,以便添加验证:
asp-validation-summary
asp-validation-for
怎么做……
Form TagHelpers 让开发者更容易在 CSHTML 视图中创建可重用组件。
- 下面是一个使用 form TagHelper 的典型表单:
<form asp-area="" asp-controller="Product" asp-action="AddProduct"
method="post" asp-antiforgery="true">
<div asp-validation-summary="All"></div>
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" asp-for="Name" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price"></label>
<input type="text" asp-for="Price" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CategoryName"></label>
<select asp-for="CategoryName" asp-items="@Categories.GetCategories()">
<option disabled selected value="">Please select one category</option>
</select>
<span asp-validation-for="CategoryName" class="text-danger"></span>
</div>
<div>
<input type="submit" value="Add" />
</div>
</form>
- 以下是 Razor 语法中的等价代码:
@using (Html.BeginForm("AddProduct", "Product", FormMethod.Post, new { area = "" }))
{
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(x => x.Name)
@Html.TextBoxFor(x => x.Name)
@Html.ValidationMessageFor(x => x.Name)
</div>
<div class="form-group">
@Html.LabelFor(x => x.Price)
@Html.TextBoxFor(x => x.Price)
@Html.ValidationMessageFor(x => x.Price)
</div>
<div class="form-group">
@Html.LabelFor(x => x.Category)
@Html.DropDownListFor(x => x.Category,
Categories.GetCategories(), "Please select one category")
@Html.ValidationMessageFor(x => x.Category)
</div>
<div>
<input type="submit" value="Add" />
</div>
}
以编程方式创建 TagHelpers
在本菜谱中,我们将学习如何以编程方式创建一个 TagHelper 来显示菜单栏。
准备
我们创建一个空的 web 应用,在project.json
中添加Microsoft.AspNetCore.Mvc
。 我们还创建了带有索引视图的controller
类。
怎么做……
在 c#中创建自定义 TagHelper 并在 CSHTML 视图中使用它是相对容易的。
- 首先,让我们创建一个从 TagHelper 继承的
VerticalMenuTagHelper
类。 通过使用类上面的HtmlTargetElement
属性,我们将使用 vmenu 作为 HTML 元素名,在视图中使用这个 TagHelper:
namespace R3.TagHelpers
{
[HtmlTargetElement("vmenu")]
public class VerticalMenuTagHelper : TagHelper
{
public List<MenuElement> Elements { get; set; }
public override void Process(
TagHelperContext context, TagHelperOutput output)
{
output.TagName = "section";
string menuElements = "<ul>";
foreach (var e in Elements)
{
menuElements +=
$@"<li><ahref='{e.Controller}/{e.Action}'><strong>{e.Name}</strong>
</a></li>";
}
menuElements += "</ul>";
output.Content.SetHtmlContent(menuElements);
output.TagMode = TagMode.StartTagAndEndTag;
}
}
}
- 我们还创建了一个
MenuElement
类,它将作为参数注入到菜单栏中:
public class MenuElement
{
public string Name { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
}
目标是在页面上生成以下代码。 当然,在真实的环境中,我们在 Layout 页面中注入这段代码是为了在整个应用中共享这个菜单:
<section class="menustyle">
<ul>
<li><a href='Home/Index'><strong>Home</strong></a></li>
<li><a href='Home/About'><strong>About</strong></a></li>
<li><a href='Home/Contact'><strong>Contact</strong></a></li>
</ul>
</section>
- 接下来,我们创建一个控制器,我们将在其中创建一个 viewModel 来给索引视图和 TagHelper 提供信息:
public class HomeController : Controller
{
public IActionResult Index()
{
var model = new List<MenuElement>()
{
new MenuElement
{
Name = "Home",
Controller = "Home",
Action = "Index"
},
new MenuElement
{
Name = "About",
Controller = "Home",
Action = "About"
},
new MenuElement
{
Name = "Contact",
Controller = "Home",
Action = "Contact"
}
};
return View(model);
}
}
- 接下来,我们创建索引视图来调用标签帮助器:
@model List<R3.TagHelpers.MenuElement>
<html>
<head>
<link href="~/css/StyleSheet.css" rel="stylesheet" />
</head>
<body>
<vmenu elements="@Model" class="menustyle" />
</body>
</html>
- 我们可以看到,已将
menustyle
类添加到 TagHelper 中,并将 CSS 文件添加到存储静态文件的wwwroot
文件夹中。 不要忘记将UseStaticFiles
中间件添加到Startup.cs
中,以允许应用中存在静态文件。 这个类的 CSS 代码如下:
.menustyle
{}
section.menustyle ul
{
list-style-type: none;
}
section.menustyle ul li
{
display : inline;
padding : 0 0.5em;
}
- 要使所有这些工作正常进行,我们需要创建一个
_ViewImports.cshtml
,在这个 T0 中,我们将导入视图中使用的所有名称空间,包括 TagHelper 的通用名称空间和我们刚刚创建的 TagHelper 的名称空间。 我们将使用以下内容:
@using R3
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper "R3.TagHelpers.VerticalMenuTagHelper, R3"
有更多的…
在前面的示例中,插入的根 HTML 标记是section
。 我们通过将 HTML 标记名称作为output.TagName
的值来表示。 在output.Content.SetHtmlContent
中,我们将 HTML 注入 section 元素中。
创建一个可重用的视图组件
在本教程中,我们将学习如何创建视图组件。
准备
我们创建一个空的 web 应用,在project.json
中添加Microsoft.AspNetCore.Mvc
。 我们还创建了一个带有 Index View 的控制器类。
怎么做……
视图组件允许开发人员创建具有服务器端逻辑和客户端代码的组件来呈现布局。
- 首先,添加一个服务和一个
TagCloud
类,以便在一个可重用组件中显示一个标签云列表:
public interface ITagCloudService
{
Task<List<Tag>> GetTagsAsync(string userBlog);
}
public class TagCloudService : ITagCloudService
{
public async Task<List<Tag>> GetTagsAsync(string userBlog)
{
return await Task.Run(() => GetTags(userBlog));
}
private List<Tag> GetTags(string userBlog)
{
return new List<Tag>
{
new Tag { Id = 1, Name = "Asp.Net Core" },
new Tag { Id = 2, Name = "EF Core" }
};
}
}
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
}
- 在
Sartup.cs
中,我们为新创建的服务配置了依赖注入:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<ITagCloudService, TagCloudService>();
}
- 接下来,让我们用
ViewComponent
创建一个由ViewComponent
派生的类suffixed
。 这个类必须实现一个InvokeAsync
方法,它返回一个IViewComponentResult
的任务:
public class TagCloudViewComponent : ViewComponent
{
private readonly ITagCloudService _service;
public TagCloudViewComponent(ITagCloudService service)
{
_service = service;
}
public async Task<IViewComponentResult> InvokeAsync(string userBlog)
{
var viewModel = await _service.GetTagsAsync(userBlog);
return View(viewModel);
}
}
- 接下来,我们在
Views\ Shared
文件夹中创建一个名为Components
的文件夹。 我们必须创建一个名为ViewComponent
的文件夹,我们刚刚在Components
文件夹中创建了这个文件夹。 在这个文件夹中,我们创建一个名为Default.cshtml
的视图,在其中添加以下代码:
@model List<R4.ViewComponents.Tag>
<ul>
@foreach(var t in Model)
{
<li>@t.Name</li>
}
</ul>
- 现在,我们可以用两种方式来使用这个视图组件:
public IActionResult Index()
{
return View();
}
将以下代码添加到Index.cshtml
:
@using R4.ViewComponents
@await Component.InvokeAsync("TagCloud")
public IActionResult Index()
{
return ViewComponent("TagCloud");
}
它是如何工作的…
视图组件可以替代ChildAction
或分部视图。 它包含了更多的逻辑。
它将返回编码或未编码的 HTML 片段,这取决于我们从视图组件(ViewComponentResult
、ContentViewComponentResult
或HtmlContentViewComponentResult
)返回的类型。
它可以是同步的,也可以是异步的。
它可以访问控制器可以访问的所有对象(Request
、RouteData
、User
、ViewBag
、ModelState
等)和父视图上下文(通过ViewContext
对象)。
它可以在视图中调用,也可以作为分部视图,或者由控制器作为ViewComponentResult
返回。
AViewComponent
由:
- 一个包含逻辑的类。 这个类可以派生自
ViewComponent
和/或ViewComponentAttribute
类。 作为ViewComponent
的类只能从ViewComponent
类派生。 然而,它必须包含一个Invoke()
方法。 此Invoke()
方法可以接受来自高层视图的参数。 这个Invoke()
方法可以返回:View
对象(适用于部分视图)Content
对象(用于 HTML 片段)
- 视图(不是强制的)。
如果一个局部视图被一个视图组件返回,Razor 会在以下位置搜索它:
/Views/{ControllerNameWhichContainsTheViewComponent}/Components/{ViewComponentName}/Default.cshtml
/Views/Shared/Components/{ViewComponentName}/Default.cshtml
ViewComponent
类可以被ConfigureServices
方法中配置的任何服务注入,并且可以进行单元测试。
ViewComponent
的一个不便之处是,它不能像局部视图那样由 Ajax 进行更新。 另一个不便之处是它总是属于父视图,但我们将在下一个 recipe 中看到如何摆脱这个约束。
创建一个视图组件/控制器类
在这个食谱中,我们将学习如何使用/创建一个混合的ViewComponent
/controller
类。
准备
我们将用 ASP 创建一个空的 web 应用.NET Core MVC 启用,将 MVC 依赖添加到项目:
"Microsoft.AspNetCore.Mvc": "2.0.0"
在本食谱中,我们将创建一个 basket 组件,并将其放在_Layout.cstml
文件中。
这样,每个页面都可以看到篮子组件。 basket 对象将存储在 Session 中。
怎么做……
- 让我们创建显示要添加到篮子中的一些产品所需的所有代码。 模型必须是可序列化的(模型,服务):
[Serializable]
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
[Serializable]
public class Basket
{
public List<Product> ListProducts { get; set; }
public decimal Total { get; set; }
public Basket()
{
ListProducts = new List<Product>();
Total = 0;
}
}
public interface IProductRepository
{
IEnumerable<Product> GetProducts();
}
public class ProductRepository : IProductRepository
{
private List<Product> _products;
public ProductRepository()
{
_products = new List<Product>()
{
new Product { Id = 1, Name = "Laptop", Price = 250 },
new Product { Id = 2, Name = "Phone", Price = 150 },
new Product { Id = 3, Name = "Screen", Price = 200 }
};
}
public IEnumerable<Product> GetProducts()
{
return _products;
}
}
- 我们创建代码是为了启用 Session 机制、
ISession
对象的一些扩展以及Startup.cs
中的一些配置。
ASP.NET Core 会话机制默认情况下不包括对象序列化,所以我们必须创建一些扩展方法来允许我们在会话中插入 Clr 对象。 在Startup.cs
中,我们将添加services.AddDistributedMemoryCache()
指令,它为我们的应用启用会话。
services.AddSession()
指令添加一个会话 cookie。
,services.AddSingleton<IHttpContextAccessor
,HttpContextAccessor>()
指令将允许我们在注入HttpContextAccessor
的应用的任何类中访问会话对象:
public static class SessionExtensions
{
public static T Get<T>(this ISession session, string key)
{
var obj = session.Get(key);
if (obj == null)
return default(T);
return Deserialize<T>(obj);
}
public static void Set<T>(this ISession session, string key, T obj)
{
if (obj != null)
session.Set(key, Serialize(obj));
}
private static byte[] Serialize(object o)
{
if (o == null)
{
return null;
}
BinaryFormatter binaryFormatter = new BinaryFormatter();
using (MemoryStream memoryStream = new MemoryStream())
{
binaryFormatter.Serialize(memoryStream, o);
byte[] objectDataAsStream = memoryStream.ToArray();
return objectDataAsStream;
}
}
private static T Deserialize<T>(byte[] stream)
{
if (stream == null)
return default(T);
BinaryFormatter binaryFormatter = new BinaryFormatter();
using (MemoryStream memoryStream = new MemoryStream(stream))
{
T result = (T)binaryFormatter.Deserialize(memoryStream);
return result;
}
}
}
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.CookieName = ".Session";
});
services.AddSingleton
<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IProductRepository, ProductRepository>();
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
...
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
- 我们创建一个名为
ProductController
的新控制器。
为了使它成为一个混合视图组件/控制器,我们必须做两件事:
[ViewComponent(Name = "BasketComponent")]
public class ProductController : Controller
{
private readonly IProductRepository _productRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
public ProductController(IProductRepository productRepository, IHttpContextAccessor httpContextAccessor)
{
_productRepository = productRepository;
_httpContextAccessor = httpContextAccessor;
}
[HttpGet]
public IActionResult Index()
{
var products = _productRepository.GetProducts();
return View(products);
}
public IViewComponentResult Invoke()
{
Basket basket;
if (_httpContextAccessor.HttpContext.Session == null)
throw new Exception("Session is not enabled !");
else
basket = _httpContextAccessor.HttpContext
.Session
.Get<Basket>("basket");
if (basket == null) basket = new Basket();
return new ViewViewComponentResult()
{
ViewName = "BasketData",
ViewData = new ViewDataDictionary<Basket>(ViewData, basket)
};
}
}
- 我们在
Views/Shared/BasketComponent/
文件夹中创建一个与Invoke
方法返回的 view 对应的视图组件,称为BasketData.cshtml
:
@model Basket
<span>
<span>Basket </span>
<span>Items : @Model.ListProducts.Count</span>
<span>Total : @Model.Total</span>
</span>
- 我们通过以下代码调用
_Layout.cshtml
视图中的视图组件:
@await Component.InvokeAsync("BasketComponent")
- 我们为
ProductController
的Index
操作方法创建索引视图。 该视图显示产品列表,但也允许我们通过AddToBasket
按钮将任何产品添加到购物篮中。 我们添加了 jQuery 代码来对AddToBasket
方法进行 AJAX 调用:
@model List<Product>
<br />
<div style="width:300px;">
@foreach (var p in Model)
{
<fieldset>
<div><span>Name: </span><span>@p.Name</span></div>
<div><span>Price: </span><span>@p.Price</span></div>
<div>
<input type="hidden" value="@p.Id" />
<input type="button" class="basket" value="Add to basket" />
</div>
</fieldset>
<br />
}
</div>
@section scripts{
<script>
$(function () {
$("input[type=button]").click(function (e) {
e.preventDefault();
var id = $(this).prev().val();
$.ajax({
type: "POST",
url: "/Home/AddToBasket",
data: { "id": id },
dataType: 'json',
success: function () {
window.location.reload();
}
});
});
});
</script>
}
- 我们在
HomeController
中添加AddToBasket
动作方法:
public class HomeController : Controller
{
private readonly IProductRepository _productRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
public HomeController(IProductRepository productRepository,
IHttpContextAccessor httpContextAccessor)
{
_productRepository = productRepository;
_httpContextAccessor = httpContextAccessor;
}
[HttpGet]
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult AddToBasket(int id)
{
var basket = _httpContextAccessor.HttpContext
.Session.Get<Basket>("basket");
if (basket == null) basket = new Basket();
var product = _productRepository.GetProducts()
.Where(p => p.Id == id).SingleOrDefault();
basket.ListProducts.Add(product);
basket.Total += product.Price;
_httpContextAccessor.HttpContext.Session.Set("basket", basket);
return NoContent();
}
}
- 最后,我们可以看到每个产品添加后篮子更新的结果:
它是如何工作的…
视图组件/控制器被创建为继承自Controller
类的控制器。 它可以是一个混合对象,如果:
- 它由
ViewComponent
属性和ViewComponent
名称作为参数装饰:
[ViewComponent(Name = "BasketComponent")]
public class ProductController : Controller
- 它包含一个返回
IViewComponentResult
的Invoke()
方法。 返回的ViewViewComponent
对象的ViewData
属性是.cshtml
视图组件的名称:
public IViewComponentResult Invoke()
{
List<string> listeData = new List<string>();
return new ViewViewComponentResult()
{
ViewName = "BasketData",
ViewData = new ViewDataDictionary<IEnumerable<string>>
(ViewData, listeData)
};
}
当它被调用时,搜索的位置将是:
/Views/Home/Components/BasketComponent/BasketData.cshtml
/Views/Shared/Components/BasketComponent/BasketData.cshtml
版权属于:月萌API www.moonapi.com,转载请注明出处