ASP.NET Core MVC视图、局部视图及布局

翻译文章,原文链接

在MVC模式中,视图处理应用程序的数据展示和用户交互。它们还有助于通过将用户界面标记与应用程序的其他部分分离,建立MVC应用程序中的关注点分离(SoC)。

视图是一个包含嵌入式Razor标记的HTML模板。它具有.cshtml扩展名,并基于C#。Razor标记与HTML标记交互,产生然后发送给客户端的网页。

通常,每个控制器操作方法都有相应的单独视图文件,视图文件被分组到以各个控制器命名的文件夹中。视图存储在应用程序根目录下的Views文件夹中。

让我们看看我们在上一篇文章中创建的BookStore应用程序:

BooksController的视图位于Views文件夹内的Books文件夹中。Books文件夹包含CreateDeleteDetailsEditIndex方法的视图。当用户请求其中一个动作时,BooksController中的动作方法使用适当的视图构建网页并将其返回给用户。

在这篇文章中,我们将重用我们在上一部分中创建的模型和控制器,但会做一些小的更改。但是,我们将从头开始创建视图。

我们强烈建议访问本系列的完整导航:ASP.NET Core MVC系列

要下载本文的源代码,请访问:视图、局部视图和布局源代码

定义 Model & Controller

让我们使用已有的model;

public class Book
{
    public int Id { get; set; }
    [Display(Name = "Book Title")]
    [Required]
    public string Title { get; set; }
    public string Genre { get; set; }
    [DataType(DataType.Currency)]
    [Range(1, 100)]
    public decimal Price { get; set; }
    [Display(Name = "Publish Date")]
    [DataType(DataType.Date)]
    public DateTime PublishDate { get; set; }
}

为了简化,我们将从头开始实现indexviewsedit。这将涵盖我们在创建视图时遇到的所有常见情况。

现在,让我们通过移除我们不使用的操作方法来稍微修改现有的控制器:

public class BooksController : Controller
{
    private readonly BookStoreWithDataContext _context;
    public BooksController(BookStoreWithDataContext context)
    {
        _context = context;
    }
    // GET: Books
    public async Task<IActionResult> Index()
    {
        return View(await _context.Book.ToListAsync());
    }
    // GET: Books/Details/5
    public async Task<IActionResult> Details(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }
        var book = await _context.Book
            .FirstOrDefaultAsync(m => m.Id == id);
        if (book == null)
        {
            return NotFound();
        }
        return View(book);
    }
    // GET: Books/Edit/5
    public async Task<IActionResult> Edit(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }
        var book = await _context.Book.FindAsync(id);
        if (book == null)
        {
            return NotFound();
        }
        return View(book);
    }
    // POST: Books/Edit/5
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Genre,Price,PublishDate")] Book book)
    {
        if (id != book.Id)
        {
            return NotFound();
        }
        if (ModelState.IsValid)
        {
            try
            {
                _context.Update(book);
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!BookExists(book.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
            return RedirectToAction(nameof(Index));
        }
        return View(book);
    }
    private bool BookExists(int id)
    {
        return _context.Book.Any(e => e.Id == id);
    }
}

我们现在已经准备好了模型和控制器。下一步是创建视图。

使用Razor标记创建视图

特定于控制器的视图将被放置在Views/[ControllerName]文件夹中。在多个控制器之间共享的视图则放置在Views/Shared文件夹中。

为了创建一个视图,让我们添加一个新文件,并以其关联的控制器操作的名称命名,文件扩展名为.cshtml

例如,要创建与BooksController中的Index操作对应的视图,我们需要在Views/Books文件夹中创建一个Index.cshtml文件。这样,我们就有了一个索引页面的视图。

在本系列的第一部分中,我们使用HTML助手方法创建了视图。在这篇文章中,我们将使用标签助手(tag helpers)来创建视图的不同方法。

标签助手提供了一个HTML友好的开发体验。在大多数情况下,使用标签助手的Razor标记看起来像标准HTML。标签助手减少了Razor视图中HTML和C#之间的明确转换。

在许多情况下,标签助手提供了特定HTML助手的替代方法,但重要的是要理解标签助手无法替代HTML助手,因为一些HTML助手没有对应的标签助手。因此,在某些情况下,我们仍然必须使用HTML助手。

index 视图

现在让我们为index页面创建视图:

@model IEnumerable<WorkingWithViews.Models.Book>
@{
    ViewData["Title"] = "Index";
    Book firstBook = Model.ToList().FirstOrDefault();
}
<h1>Index</h1>
<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                <label asp-for="@firstBook.Id"></label>
            </th>
            <th>
                <label asp-for="@firstBook.Title"></label>
            </th>
            <th>
                <label asp-for="@firstBook.Genre"></label>
            </th>
            <th>
                <label asp-for="@firstBook.Price"></label>
            </th>
            <th>
                <label asp-for="@firstBook.PublishDate"></label>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    <label>@item.Id</label>
                </td>
                <td>                    
                    <label>@item.Title</label>
                </td>
                <td>                    
                    <label>@item.Genre</label>
                </td>
                <td>                    
                    <label>@item.Price</label>
                </td>
                <td>                    
                    <label>@item.PublishDate</label>
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a>
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a>
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

在这里,我们使用的是强类型模型,且模型的类型为IEnumerable<Book>

首先,我们声明一个变量并将列表中的第一本书分配给它。我们这样做是为了获取属性名称,并将它们分配给表头:

Book firstBook = Model.ToList().FirstOrDefault();

asp-for 属性将指定的模型属性名称提取到渲染的HTML中。那么,让我们看看如何渲染一个带有Title属性名称的标签:

 <label asp-for="@firstBook.Title"></label>

棒极了。

现在我们需要渲染books集合中的所有项目。为此,我们使用foreach循环来帮助我们渲染一个HTML表格。我们可以渲染带有属性值的标签:

<label>@item.Title</label>

为了创建动作链接,我们可以使用 asp-action 属性,而为了传递参数,我们可以使用 asp-route-{parametername} 格式。因此,在这种情况下,对于id参数,我们使用 asp-route-id

<a asp-action="Edit" asp-route-id="@item.Id">Edit</a>

Details view

现在,让我们创建detail视图。

@model WorkingWithViews.Models.Book
@{
    ViewData["Title"] = "Details";
}
<h1>Details</h1>
<div>
    <h4>Book</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            <label asp-for="Title"></label>
        </dt>
        <dd class="col-sm-10">
            @Model.Title
        </dd>
        <dt class="col-sm-2">
            <label asp-for="Genre"></label>            
        </dt>
        <dd class="col-sm-10">            
            @Model.Genre
        </dd>
        <dt class="col-sm-2">
            <label asp-for="Price"></label>            
        </dt>
        <dd class="col-sm-10">            
            @Model.Price.ToString("C")
        </dd>
        <dt class="col-sm-2">
            <label asp-for="PublishDate"></label>            
        </dt>
        <dd class="col-sm-10">            
            @Model.PublishDate.ToShortDateString()
        </dd>
    </dl>
</div>
<div>    
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a>|
    <a asp-action="Index">Back to List</a>
</div>

我们已经创建了类似于index视图的内容。但模型的类型是Book。为了检索index名称,我们可以使用asp-for属性:

<label asp-for="Title"></label>

为了显示属性值,我们可以使用 @Model 指令访问模型属性:

<dd class="col-sm-10">
    @Model.Title
</dd>

Edit view

一旦我们完成了detail视图的制作,我们就可以继续创建edit视图:

@model WorkingWithViews.Models.Book
@{
    ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>Book</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Genre" class="control-label"></label>
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Price" class="control-label"></label>
                <input asp-for="Price" class="form-control" />
                <span asp-validation-for="Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="PublishDate" class="control-label"></label>
                <input asp-for="PublishDate" class="form-control" />
                <span asp-validation-for="PublishDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>
<div>
    <a asp-action="Index">Back to List</a>
</div>

对于edit视图,模型也是Book类型。

asp-validation-summary 标签助手用于显示验证摘要:

<div asp-validation-summary="ModelOnly" class="text-danger"></div>

ValidationSummary.ModelOnly 仅显示适用于模型级别的验证消息。ValidationSummary.All 将显示属性级和模型级的验证消息。

对于每个属性,我们创建了一个标签用于显示属性名称,一个输入字段用于编辑值,以及一个 span 元素用于显示特定于该属性的验证消息:

<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>

我们在底部有一个按钮用于提交表单:

<input type="submit" value="Save" class="btn btn-primary" />

当我们尝试在不提供有效值的情况下保存页面时,将根据我们为asp-validation-summary提供的设置显示验证错误。所以,让我们看看这在实际操作中是如何进行的。

当我们将值设置为ModelOnly时:

如果设置值为All:

就是这样。我们已经为indexdetailedit页面创建了视图。

部分视图的概念

部分视图是一个Razor标记文件(.cshtml),它在另一个视图的渲染输出中渲染HTML输出。

部分视图在两种情况下非常有用。第一种情况是当我们想要将大型标记文件分解成更小的组件时。如果我们的标记文件很大、复杂并由几个逻辑部分组成,我们应该将每个部分分解为一个部分视图。然后,标记文件中的代码将会更易于管理,因为标记将只包含整个页面结构和对部分视图的引用。

第二种情况是当我们想要减少跨标记文件的常见标记内容的重复时。当我们需要在标记文件中使用相同的标记元素时,我们可以将该标记内容移动到一个部分视图中并重用它。这样,将来对该标记的更改只需要在一个地方进行,我们就提高了代码的模块化。

然而,部分视图不是维护常见布局元素的推荐方法。在下一节中,我们将学习创建常见布局元素的最佳实践。

假设我们需要在BookStore应用程序的多个地方显示作者信息。创建一个用于显示作者信息的部分视图将是理想的做法。

右击Shared文件夹并选择添加 -> 视图:

在“添加MVC视图”对话框中,我们将视图名称设置为_Authors,然后勾选“创建为部分视图”选项,点击添加:

让我们向_authors.cshtml文件中添加一些虚拟文本:

<h3>Authors</h3>
<p>This section is used to display information about authors.</p>

现在,让我们使用partial标签助手将这个部分视图添加到书籍详情视图中:

<div>
        <partial name="_Authors" />
</div>

就是这样。我们可以看到,书籍详细信息页面现在也显示了“作者”部分:

我们可以通过在其他视图中放置这个部分视图来重用这一部分。

在这一部分中,我们学习了如何创建部分视图以及如何在视图中使用它。

ASP.NET Core中的布局

大多数Web应用程序都有一个共同的布局,为用户在页面间导航时提供一致的体验。在ASP.NET Core MVC应用程序中,我们使用布局文件为整个页面提供一致的体验。

布局通常包括诸如头部、菜单和页脚之类的常见用户界面元素。应用程序中的许多页面共享诸如脚本和样式表之类的公共资源。我们可以在一个布局文件中定义所有这些共享元素,然后该布局文件可以被应用程序中的任何视图引用。布局有助于减少视图中的重复代码。

当我们使用Visual Studio提供的默认模板创建一个ASP.Net Core MVC应用程序时,它会生成一个默认的布局文件(_Layout.cshtml)并将其放置在Shared文件夹中。在创建视图时,我们可以选择指定一个布局文件。我们可以稍后通过设置视图的Layout属性来更改这一点:

现在让我们来检查默认的布局文件。

布局文件的顶部包含一个 <head> 部分,其中包含标题、指向样式表等的链接。

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WorkingWithViews</title>
    <link rel="stylesheet" href="~/css/site.css" />
</head>

接着是包含带有菜单的页眉的 <body> 部分。<body> 还有一个容器 div,在其中调用了 RenderBody() 方法。这个方法渲染内容页:

<div class="container">
    <partial name="_CookieConsentPartial" />
    <main role="main" class="pb-3">
        @RenderBody()
    </main>
</div>

接下来是一个 <footer> 部分。

我们通常在文档末尾加载脚本,以确保加载所有依赖项:

<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)

在这一部分中,我们学习了如何使用布局文件为我们的应用程序维护一致的外观和感觉。

总结

在这篇文章中,我们探讨了以下主题:

使用Razor标记构建视图
使用部分视图重用页面的部分内容
使用布局文件为应用程序创建共同的外观和感觉
在本系列的下一部分中,我们将探讨ASP.NET Core MVC中的状态管理。

发表回复

您的电子邮箱地址不会被公开。