十四、Razor 和视图

在本章中,我们将介绍:

  • 使用 ViewImports 管理视图中的名称空间
  • 创建强类型的 Partial 视图
  • 配置视图和区域位置
  • 在视图中使用依赖注入
  • 创建 HTMLHelpers

使用 ViewImports 管理视图中的名称空间

在本菜谱中,您将学习如何在Razor视图中创建和管理名称空间,方法是创建_ViewImport视图,在应用视图中添加所有必要的名称空间。

准备

我们创建一个空的 web 应用,其中包含一个控制器和一个带有相应文件夹的index视图。

怎么做……

TagHelpers 的目标是减少使用 HTML 标记和/或由Razor解释的属性编写视图。 它们用最接近 HTML 的语法替换 HTMLHelpers,允许集成器更容易地使用 MVC 视图:

  1. 首先,在HomeController.cs中为GETPOST添加一个Insert动作方法,在POSTInsert方法上添加一个断点:

  1. 接下来,让我们为输入添加带有 TagHelper 代码的Insert视图,以测试 TagHelper:

  • 我们可以看到在输入文本 HTML 元素中添加了asp-forTagHelper。

  • 让我们测试一下POST方法,在输入中添加一个值:

  1. 我们点击 Send 按钮,看看在POSTInsert方法中发生了什么:

  1. 输入为空; 现在,让我们纠正它。 添加一个_ViewImports.cshtml页面通过右键单击Views文件夹,并选择添加|新项目| MVC 视图导入页面:

  1. 接下来,我们将以下代码添加到_ViewImports.cshtml中,以允许在Razor视图中使用 TagHelpers,导入Microsoft.AspNetCore.Mvc.TagHelpers命名空间:

  1. 最后,让我们在Insert视图中再次测试POST方法:

  • 我们可以看到 TagHelpers 现在在我们的视图中工作。

创建强类型的 Partial 视图

在本教程中,您将了解什么是Partial视图,以及如何创建强类型视图。

准备

我们将用 ASP 创建一个空的 web 应用.NET Core MVC 启用,将 MVC 依赖添加到项目中:

"Microsoft.AspNetCore.Mvc": "2.0.0"

怎么做……

我们可以利用 c#编译器的强类型 Partial 视图。 c#编译器检查属性的名称和类型。 如果它检测到误用(比如错误的属性名,等等),它将抛出一个异常,并且不会编译项目:

  1. 首先,让我们创建DtoViewModel对象:
public class ProductDto
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal Price { get; set; }
}
public class ProductViewModel
{
  public int Id { get; set; }
  [Required]
  [MaxLength(50)]
  public string Name { get; set; }
  [Required]
  [Range(0.01, double.MaxValue,
  ErrorMessage = "Please enter a positive number")]
  public decimal Price { get; set; }
}
  1. 接下来,我们创建控制器用来检索ProductViewModel的服务层:
public interface IProductRepository
{
  IEnumerable<ProductDto> GetProducts();
}
public class ProductRepository : IProductRepository
{
  private List<ProductDto> _products;
  public ProductRepository()
  {
    _products = new List<ProductDto>()
    {
      new ProductDto { Id = 1, Name = "Laptop" },
      new ProductDto { Id = 2, Name = "Phone" },
      new ProductDto { Id = 3, Name = "Desktop" }
    };
  }
  public IEnumerable<ProductDto> GetProducts()
  {
    return _products.AsEnumerable();
  }
}
  1. 接下来,我们配置Startup.cs来管理存储库生命周期和自动映射器:
services.AddScoped<IProductRepository, ProductRepository>();
AutoMapper.Mapper.Initialize(mapper =>
{
  mapper.CreateMap<Models.ProductDto, 
  Models.ProductViewModel>();
});
  1. 现在,让我们创建一个使用服务层的ProductController:
public class ProductController : Controller
{
  private readonly IProductRepository _productRepository;
  public ProductController(IProductRepository productRepository)
  {
    _productRepository = productRepository;
  }
  public IActionResult Index()
  {
    var productsDtoFromRepo = _productRepository.GetProducts();
    var model = Mapper.Map
    <IEnumerable<ProductViewModel>>
    (productsDtoFromRepo);
    return View(model);
  }
}
  1. 现在,让我们创建ProductControllerindex视图,它将包含强类型Partial视图:
@model List<ProductViewModel>
<br />
@foreach (var product in Model)
{
 @Html.Partial("ProductPartial", product) }
  • 这里,for循环中的代码调用Html.PartialProductPartial是分部视图的名称。 这个 Partial 视图必须位于Product Views文件夹或共享文件夹中。 我们可以看到传递的第二个参数是一个product对象。

  • 现在,让我们看看ProductPartial.cshtml视图中的Partial代码:

@model ProductViewModel
<p>Product @Model.Id : @Model.Name</p>

配置视图和区域位置

在本食谱中,您将学习如何更改 ASP 使用的每个约定的视图位置.NET Core MVC。

准备

我们将使用前面的配方并添加一些修改以使用不同的视图位置。

怎么做……

  1. 首先,让我们创建一个名为ViewsOld的新视图文件夹。 在这个文件夹中,我们将从Views文件夹中复制_ViewImports.cshtml_ViewSart/cshtml,并剪切Productviews 文件夹粘贴到ViewsOld文件夹中:

  1. 接下来,我们创建一个名为Infrastructure的文件夹,并创建一个名为OldViewsExpander.cs的 c#类。 该类将包含以下代码:
public class OldViewsExpander : IViewLocationExpander
{
  public IEnumerable<string> ExpandViewLocations
  (ViewLocationExpanderContext context,
  IEnumerable<string> viewLocations)
  {
    // rerturn all classic locations
    foreach (string location in viewLocations)
    {
      yield return location;
    }
    // return new locations
    yield return "/ViewsOld/Shared/{0}.cshtml";
    yield return "/ViewsOld/{1}/{0}.cshtml";
    // now the following locations will be tested
    // "/Views/Home"
    // "/Views/Product"
    // "/Views/Shared"
    // "/ViewsOld/Home"
    // "/ViewsOld/Product"
    // "/ViewsOld/Shared"
  }
  public void PopulateValues(ViewLocationExpanderContext context)
  {}
}
  1. 为了在应用中包含这个新配置,我们必须将新的ViewLocationExpanders添加到ViewLocationExpanders应用的列表中:
public void ConfigureServices(IServiceCollection services)
{
  services.AddScoped<IProductRepository, ProductRepository>();
  services.AddMvc();
 services.Configure<RazorViewEngineOptions>(options => 
  {
 options.ViewLocationExpanders.Clear(); options.ViewLocationExpanders.Add( new OldViewsExpander());
 }); }
  1. 我们还可以通过将模板视图直接添加到ViewLocationFormats列表中,而不使用OldViewsExpander类来修改或添加整个应用的区域位置:
public void ConfigureServices(IServiceCollection services)
{
  services.AddScoped<IProductRepository, ProductRepository>();
  services.AddMvc();
 services.Configure<RazorViewEngineOptions>(options => 
  {
 options.ViewLocationFormats.Add( "/ViewsOld/Shared/{0}.cshtml");
 options.ViewLocationFormats.Add( "/ViewsOld/{1}/{0}.cshtml"); 
  }); }
  1. 为了只使用ViewsOld文件夹作为整个应用的视图位置,我们必须使用以下代码,在添加新位置之前清除ViewLocationFormats列表。 在此之前,我们将共享文件夹及其_Layout.cshtml文件从Views文件夹复制到OldViews文件夹,以避免出现错误。 如果我们不移动共享文件夹和_Layout.cshtml文件,_ViewStart.cshtml将在共享文件夹中搜索其布局文件,并且不使用_Layout.cshtml作为其布局:
public void ConfigureServices(IServiceCollection services)
{
  services.AddScoped<IProductRepository, ProductRepository>();
  services.AddMvc();
 services.Configure<RazorViewEngineOptions>(options => 
  {
 options.ViewLocationFormats.Clear(); options.ViewLocationFormats.Add( "/ViewsOld/Shared/{0}.cshtml");
 options.ViewLocationFormats.Add( "/ViewsOld/{1}/{0}.cshtml");
 }); }

它是如何工作的…

为了修改或添加View位置文件夹,我们创建了一个继承了IViewLocationExpander接口的类。

如果我们没有添加以下代码,我们就不需要包含本机Views文件夹位置; 我们将替换它:

foreach(string location in viewLocations)
{
  yield return location;
}

MVC 机制搜索Views的两个本地文件夹是:

"/Views/{ControllerName}"
 "/Views/Shared"

现在,通过下面的代码,我们将添加新的Views位置:

yield return "/ViewsOld/Shared/{0}.cshtml";
yield return "/ViewsOld/{1}/{0}.cshtml";

我们可以在不使用OldViewsExpander类的情况下获得相同的结果,只需将以下代码添加到ConfigureServices方法:

services.Configure<RazorViewEngineOptions>(options => 
{
 options.ViewLocationFormats.Add(
   "/ViewsOld/Shared/{0}.cshtml");
 options.ViewLocationFormats.Add(
   "/ViewsOld/{1}/{0}.cshtml"); });

现在,一个视图可能的文件夹位置可以是:

"/Views/{ControllerName}"
 "/Views/Shared"
 "/ViewsOld/{ControllerName}"
 "/ViewsOld/Shared"

为了完成这项工作,我们需要为遗留区域添加一个新的文件夹模式,如下代码所示:

services.Configure<RazorViewEngineOptions>(options =>
{
  options.ViewLocationFormats.Add(
  "/ViewsOld/Shared/{0}.cshtml");
  options.ViewLocationFormats.Add(
  "/ViewsOld/{1}/{0}.cshtml");
 options.AreaViewLocationFormats.Add(
 "/AreasOld/{2}/Views/{1}/{0}.cshtml");
 options.AreaViewLocationFormats.Add(
 "/AreasOld/{2}/Views/Shared/{0}.cshtml"); });
Legend:
 // {0} - Action Name
 // {1} - Controller Name
 // {2} - Area Name

在视图中使用依赖注入

在本教程中,您将了解什么是依赖项注入以及如何在 ASP 中使用它.NET Core MVC 视图。 我们将看到如何注入一个包含类别对象的列表来绑定AddProduct.cshtml视图中的下拉列表。

依赖注入机制让类通过构造函数参数获得对象,而不是调用工厂方法,如下代码:

public ClassA (ClassB _object) { 
    this.object = _object; 
} 

使用下面的代码代替前面的代码:

public ClassA() { 
    this.object = FactoryClass.GetObjectB(); 
} 

准备

我们将用 ASP 创建一个空的 web 应用.NET Core MVC 通过添加 MVC 依赖到项目中启用:

"Microsoft.AspNetCore.Mvc": "2.0.0"

怎么做……

  1. 首先,让我们创建DtoViewModel对象:
public class ProductDto
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal Price { get; set; }
}
public class ProductViewModel
{
  public int Id { get; set; }
  [Required]
  [MaxLength(50)]
  public string Name { get; set; }
  [Required]
  [Range(0.01, double.MaxValue,
  ErrorMessage = "Please enter a positive number")]
  public decimal Price { get; set; }
 [Required]
 public string CategoryName 
  {
 get; set; 
  } }
  1. 接下来,我们创建控制器用于检索ProductViewModel的服务层。 产品控制器将使用这个存储库。 这个存储库使用一个静态的、硬编码的数据存储,使数据在 HTTP 请求之间持久:
public class ProductDataStore
{
  public static ProductDataStore Current { get; }
  = new ProductDataStore();
  public List<ProductDto> Products { get; set; }
  public ProductDataStore()
  {
    Products = new List<ProductDto>()
    {
      new ProductDto { Id = 1, Name = "Laptop" },
      new ProductDto { Id = 2, Name = "Phone" },
      new ProductDto { Id = 3, Name = "Desktop" }
    };
  }
}
public interface IProductRepository
{
  IEnumerable<ProductDto> GetProducts();
}
public class ProductRepository : IProductRepository
{
  public List<ProductDto> Products
  {
    get
    {
      return ProductDataStore.Current.Products;
    }
  }
  public IEnumerable<ProductDto> GetProducts()
  {
    return Products.AsEnumerable();
  }
}
  1. 为了在视图中注入数据,我们将使用另一个存储库,它从 JSON 文件中读取数据。 首先,我们将使用No schema selected在应用根目录下创建一个 JSON 文件。 CategoryJSONDataStore.json如下所示:
{
 "Categories": [
 "Computers", 
  "Software",
 "Electronics",
 "TV",
 "DVD"] }
  1. 然后,我们创建CategoryRepository,它使用这个 JSON 文件并将其发送给视图。 显然,这些数据应该来自一个数据库:
public interface ICategoryRepository
{
  List<SelectListItem> GetCategories();
}
public class CategoryRepository: ICategoryRepository
{
  private CategoryFormData _data;
  public CategoryRepository()
  {
    _data = JSONConvert
    .DeserializeObject<CategoryFormData>(
     File
    .ReadAllText("CategoryJSONDataStore.json"));
  }
  public List<SelectListItem> GetCategories()
  {
    return _data.Categories.Select
    (x => new SelectListItem() { Text = x }).ToList();
  }
}
public class CategoryFormData
{
  public List<string> Categories 
  {
    get; set; 
  }
}
  1. 接下来,我们配置Startup.cs来管理存储库生命周期,而AutoMapper:
    • ConfigureServices方法中:
services.AddScoped<IProductRepository, 
ProductRepository>();
services.AddSingleton<ICategoryRepository, 
CategoryRepository>();
AutoMapper.Mapper.Initialize(mapper => {
  mapper.CreateMap<Models.ProductDto, 
  Models.ProductViewModel>();
  mapper.CreateMap<Models.ProductViewModel, 
  Models.ProductDto>();
});
  1. 现在,让我们创建一个使用服务层的ProductController:
public class ProductController : Controller
{
  private readonly IProductRepository 
  _productRepository;
  public ProductController
  (IProductRepository productRepository)
  {
    _productRepository = productRepository;
  }
  public IActionResult Index()
  {
    var productsDtoFromRepo = 
    _productRepository.GetProducts();
    var model =
    Mapper.Map<IEnumerable<ProductViewModel>>
    (productsDtoFromRepo);
    return View(model);
  }
  [HttpGet]
  public IActionResult AddProduct()
  {
    return View();
  }
  [HttpPost]
  [ValidateAntiForgeryToken]
  public IActionResult AddProduct(ProductViewModel model)
  {
    if (ModelState.IsValid)
    {
      model.Id = _productRepository.GetProducts()
      .Max(p => p.Id) + 1;
      var productDto = Mapper.Map<ProductDto>(model);
      _productRepository.AddProduct(productDto);
      return RedirectToAction("Index");
    }
    return View(model);
  }
}
  1. 让我们添加IndexAddProduct动作的视图:
    • Index.cshtml:
@model List<ProductViewModel>
<br />
@foreach (var p in Model)
{
  <p>Product @p.Id : @p.Name</p>
}
@model ProductViewModel
@inject
 ICategoryDataStore
 Categories <br />
<p>Let's add a new product</p>
@using (Html.BeginForm("AddProduct", "Product", FormMethod.Post))
{
  <div class="row">
    <div class="col-md-6">
      @Html.ValidationSummary(false)
      <div class="form-group">
        @Html.LabelFor(x => x.Name, "Product Name")
      @Html.TextBoxFor(x => x.Name,
      new { @class = "form-control" })
      @Html.ValidationMessageFor(x => x.Name)
    </div>
    <div class="form-group">
      @Html.LabelFor(x => x.Price, "Product Price")
      @Html.TextBoxFor(x => x.Price,
      new { @class = "form-control" })
      @Html.ValidationMessageFor(x => x.Price)
    </div>
    <div class="form-group">
      @Html.LabelFor(x => x.CategoryName, "Category")
      @Html.DropDownListFor(x => x.CategoryName,
 Categories.GetCategories(), "Please select one category",
      new { @class = "form-control"})
    </div>
    <div>
      <input type="submit" value="Add" />
    </div>
  </div>
</div>
@section scripts{
  <script src="~/lib/jquery-validation/dist/jquery.validate.js">
  </script>
  <script src="~/lib/jquery-validation-
  unobtrusive/jquery.validate.unobtrusive.js">
  </script>
}
  1. Views文件夹中,我们必须修改_Layout.cshtml文件和_ViewImports.cshtml文件中的一些代码,以使视图正常工作:
    • In Shared/_Layout.cshtml:
<ul class="nav navbar-nav"><li>
  <aasp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li>
<aasp-area="" asp-controller="Product" asp-action="AddProduct">Add Product</a>
</li>
</ul>
@using R4
@using R4.Models
@using R4.Services @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
  1. 如果我们启动应用,我们现在可以看到类别下拉列表是通过将CategoryRepository直接注入AddProduct视图而独立于ProductController绑定的:

创建 HTMLHelpers

在这个食谱中,您将学习如何创建我们自己的 HTMLHelpers。 下面的代码基于前面的配方,在视图中使用依赖注入。

准备

我们将用 ASP 创建一个空的 web 应用.NET Core MVC 通过添加 MVC 依赖到项目中启用:

"Microsoft.AspNetCore.Mvc": "2.0.0".

怎么做……

现在我们可以深入了解视图中的依赖注入:

  1. 首先,我们将带有相应视图和关联存储库的AddProduct操作方法添加到这个模板 web 项目中。 在本例中,产品列表和类别列表来自硬编码代码,但在现实世界中,它应该来自数据库(关系或非关系)、服务或任何其他数据源:
    • 下面的代码是DtoViewModel类:
public class ProductDto
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal Price { get; set; }
}
public class ProductViewModel
{
  public int Id { get; set; }
  [Required]
  [MaxLength(50)]
  public string Name { get; set; }
  [Required]
  [Range(0.01, double.MaxValue,
  ErrorMessage = "Please enter a positive number")]
  public decimal Price { get; set; }
 [Required]
 public string CategoryName 
  {
 get; set; 
  } }
public class ProductDataStore
{
  public static ProductDataStore Current { get; }
  = new ProductDataStore();
  public List<ProductDto> Products { get; set; }
  public ProductDataStore()
  {
    Products = new List<ProductDto>()
    { 
      new ProductDto { Id = 1, Name = "Laptop" },
      new ProductDto { Id = 2, Name = "Phone" },
      new ProductDto { Id = 3, Name = "Desktop" }
    };
  }
}
public interface IProductRepository
{
  IEnumerable<ProductDto> GetProducts();
  void AddProduct(ProductDto productDto);
}
public class ProductRepository : IProductRepository
{
  public List<ProductDto> Products
  {
    get
    {
      return ProductDataStore.Current.Products;
    }
  }
  public IEnumerable<ProductDto> GetProducts()
  {
    return Products.AsEnumerable();
  }
  public void AddProduct(ProductDto productDto)
  {
    Products.Add(productDto);
  }
}
public class HomeController : Controller
{
  private IProductRepository _productRepository;
  public HomeController(IProductRepository productRepository)
  {
    _productRepository = productRepository;
  }
  public IActionResult Index()
  {
    var productsDtoFromRepo = _productRepository.GetProducts();
    var model =
    Mapper.Map<IEnumerable<ProductViewModel>>
    (productsDtoFromRepo);
    return View(model);
  }
  [HttpGet]
  public IActionResult AddProduct()
  {
    return View();
  }
  [HttpPost]
  [ValidateAntiForgeryToken]
  public IActionResult AddProduct(ProductViewModel model)
  {
    if (ModelState.IsValid)
    {
      model.Id = _productRepository.GetProducts()
      .Max(p => p.Id) + 1;
      var productDto = Mapper.Map<ProductDto>(model);
      _productRepository.AddProduct(productDto);
      return RedirectToAction("Index");
    }
    return View(model);
  }
}
@using R5
@using R5.Models
@using R5.Services
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model ProductViewModel
@inject ICategoryDataStore Categories
<br />
<p>Let's add a new product</p>
@using (Html.BeginForm("AddProduct", "Product", FormMethod.Post))
{
  <div class="row">
    <div class="col-md-6">
      @Html.ValidationSummary(false)
      <div class="form-group">
        @Html.LabelFor(x => x.Name, "Product Name")
        @Html.TextBoxFor(x => x.Name,
        new { @class = "form-control" })
        @Html.ValidationMessageFor(x => x.Name)
      </div>
      <div class="form-group">
        @Html.LabelFor(x => x.Price, "Product Price")
        @Html.TextBoxFor(x => x.Price,
        new { @class = "form-control" })
        @Html.ValidationMessageFor(x => x.Price)
      </div>
      <div class="form-group">
        @Html.LabelFor(x => x.CategoryName, "Category")
        @Html.DropDownListFor(x => x.CategoryName,
        Categories.GetCategories(), "Please select one category",
        new { @class = "form-control"})
        @Html.ValidationMessageFor(x => x.CategoryName)
      </div>
      <div>
        <input type="submit" value="Add" />
      </div>
    </div>
  </div>
}
@section scripts
{
  <script src="~/lib/jquery-validation/dist/jquery.validate.js">
  </script>
  <script src="~/lib/jquery-validation-
  unobtrusive/jquery.validate.unobtrusive.js">
  </script>
}
  1. 接下来,我们配置Startup.cs来管理存储库的生命周期,配置AutoMapper来实现这一点:
    • ConfigureServices方法中:
services.AddScoped<IProductRepository, ProductRepository>();
services.AddSingleton<ICategoryRepository, CategoryRepository>();
AutoMapper.Mapper.Initialize(mapper => {
  mapper.CreateMap<Models.ProductDto, Models.ProductViewModel>();
  mapper.CreateMap<Models.ProductViewModel, Models.ProductDto>();
});
  1. 现在,我们可以详细查看表单中的 RazorAddProduct.cshtml代码。 参见以下代码:
<div class="form-group">
  @Html.LabelFor(x => x.Name, "Product Name")
  @Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
  @Html.ValidationMessageFor(x => x.Name)
</div>
  • 我们想用下面的代码替换前面的代码:
<div class="form-group">
  @Html.CustomTextboxFor(x => x.Name, "Product Name")
</div>
  1. 要创建这个CustomTextboxFor``HTMLHelper,我们必须创建一个静态类,其中包含一个名为CustomTextboxFor的静态扩展方法。 类名称不重要,但是类名称空间必须是应用的名称空间,以便在应用的任何地方使用:
public static class CustomHTMLHelpers
{
  public static IHtmlContent CustomTextboxFor<TModel, TResult>
  (this IHtmlHelper<TModel> htmlHelper, 
  Expression<Func<TModel, TResult>> expression, 
  string labelText)
  {
    var label = htmlHelper.LabelFor(expression, labelText);
    var textBox = htmlHelper.TextBoxFor(expression, 
    new { @class = "form-control" });
    var validation = htmlHelper.ValidationMessageFor
    (expression, null, 
    new { @class = "text-danger" });

    var writer = new StringWriter();
    label.WriteTo(writer, HtmlEncoder.Default);
    textBox.WriteTo(writer, HtmlEncoder.Default);
    validation.WriteTo(writer, HtmlEncoder.Default);

    return new HtmlString(writer.GetStringBuilder().ToString());
  }
}
  1. 现在我们将表单中的代码更改为下面的代码,我们可以看到它工作了:
@using (Html.BeginForm("AddProduct", "Product", FormMethod.Post))
{
  <div class="row">
    <div class="col-md-6">
      @Html.ValidationSummary(false)
      <div class="form-group">
        @Html.CustomTextboxFor(x => x.Name, "Product Name") 
      </div>
      <div class="form-group">
        @Html.CustomTextboxFor(x => x.Price, "Product Price") 
      </div>
      <div class="form-group">
        @Html.LabelFor(x => x.CategoryName, "Category")
        @Html.DropDownListFor(x => x.CategoryName,
        Categories.GetCategories(), "Please select one category", 
        new { @class = "form-control"})
        @Html.ValidationMessageFor(x => x.CategoryName)
      </div>
      <div>
        <input type="submit" value="Add" />
      </div>
    </div>
  </div>
}
  1. 下一个任务是,用新创建的 HTMLHelper 用法替换通用的下拉列表代码:
<div class="form-group">
  @Html.LabelFor(x => x.CategoryName, "Category")
  @Html.DropDownListFor(x => x.CategoryName,
  Categories.GetCategories(), "Please select one category", 
  new { @class = "form-control"})
  @Html.ValidationMessageFor(x => x.CategoryName)
</div>
  • 用下面的代码替换前面的代码:
<div class="form-group">
  @Html.CustomDropDownListFor(x => x.CategoryName, 
  Categories.GetCategories(), "Category")
</div>
  1. 为此,我们将以下扩展方法添加到CustomHTMLHelpers类中,命名为CustomDropDownListFor:
public static IHtmlContent CustomDropDownListFor<TModel, TResult>
(this IHtmlHelper<TModel> htmlHelper, 
Expression<Func<TModel, TResult>> expression, 
IEnumerable<SelectListItem> selectList, string labelText)
{
  var label = htmlHelper.LabelFor(expression, labelText);
  var dropdownlist = htmlHelper.DropDownListFor
  (expression, selectList, 
  "Please select one element on the list", 
  new { @class = "form-control" });
  var validation = htmlHelper.ValidationMessageFor(expression, null, 
  new { @class = "text-danger" });

  var writer = new StringWriter();
  label.WriteTo(writer, HtmlEncoder.Default);
  dropdownlist.WriteTo(writer, HtmlEncoder.Default);
  validation.WriteTo(writer, HtmlEncoder.Default);

  return new HtmlString(writer.GetStringBuilder().ToString());
}
  1. 现在表单的代码是这样的; 我们可以看到,它不那么冗长:
@using (Html.BeginForm("AddProduct", "Product", FormMethod.Post))
{
  <div class="row">
    <div class="col-md-6">
      @Html.ValidationSummary(false)
      <div class="form-group">
        @Html.CustomTextboxFor(x => x.Name, "Product Name") 
      </div>
      <div class="form-group">
        @Html.CustomTextboxFor(x => x.Price, "Product Price") 
      </div>
      <div class="form-group">
        @Html.CustomDropDownListFor
        (x => x.CategoryName, Categories.GetCategories(), "Category") 
      </div>
      <div>
        <input type="submit" value="Add" />
      </div>
    </div>
  </div>
}

它是如何工作的…

让我们详细看看静态 HTMLHelpersextension方法。 它们总是返回一个IHtmlContent(一个以StringWriter为参数的HtmlString对象)。 第一个参数是IHtmlHelper<TModel>,用于将此方法作为extension方法附加到HtmlHelper``static类。 之后,我们使用HtmlHelper创建我们想要生成的所有元素(在当前的例子中,是一个label、一个textbox和一个model属性的验证消息)。 我们将这些元素作为生成的字符串添加到返回的StringWriter的 HTML 编码格式上。