目录
介绍
在本系列中,我们将了解如何仅使用ASP.NET Core和Sircl轻松构建出色的交互式Web应用程序(通常需要大量JavaScript代码或用JavaScript框架编写的应用程序。
Sircl是一个开源的客户端库,它扩展了HTML以提供部分更新和常见行为,并可以轻松编写依赖于服务器端渲染的丰富应用程序。
在本系列的每一部分中,我们将介绍使用服务器端技术的富Web应用程序的典型“编程问题”,并了解如何使用Sircl在ASP.NET Core中解决这个问题。
在上一篇文章中,我们了解了如何将动态行为与页面部分请求相结合,以构建丰富且动态的网页。我们将继续添加列表管理,并发现一些特定于表单的功能。
- 使用ASP.NET Core和Sircl构建丰富的Web应用程序——第1部分:使用事件操作的客户端行为
- 使用ASP.NET Core和Sircl构建丰富的Web应用程序——第2部分:部分页面加载
- 使用ASP.NET Core和Sircl构建丰富的 Web 应用程序——第3部分:列表管理
- 使用ASP.NET Core和Sircl构建丰富的Web应用程序——第3b部分:列表管理(使用最少的回发)
- 使用ASP.NET Core和Sircl构建丰富的Web应用程序——第4部分:引导模式(第1部分)
列表管理
今天,我们将构建一个简单的航班登记表:用户选择航班(日期和机场)并输入一名或多名乘客。
由于事先不知道乘客人数,因此该表单提供了一种添加(和删除)乘客的方法。
或ViewModel类如下:
public class IndexModel
{
public FlightModel Flight { get; set; } = new();
public IEnumerable<SelectListItem>? FromAirports { get; internal set; }
public IEnumerable<SelectListItem>? ToAirports { get; internal set; }
}
public class FlightModel
{
[Required]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateOnly? Date { get; set; }
[Required]
public string? FromAirport { get; set; }
[Required]
public string? ToAirport { get; set; }
public List<PassengerModel> Passengers { get; internal set; } = new();
}
public class PassengerModel
{
[Required]
public string? FirstName { get; set; }
[Required]
public string? LastName { get; set; }
}
需要 Date 属性的 DisplayFormat 属性才能正确设置日期格式,以便与类型为“date”的INPUT元素一起使用。
FlightModel包含PassengerModel类型的乘客列表。
可以生成以下表单以匹配此模型:
让我们从航班详细信息(日期、往返机场)开始。一个特殊性是,目的地机场的列表将取决于所选的日期和出发机场,因为它应该只提供现有航班。
对于此模拟,我们只会确保目的地机场列表不包括出发机场,因为从同一机场到达和离开是没有意义的......
这可以通过以下Index控制器操作来实现:
public IActionResult Index(IndexModel model)
{
ModelState.Clear();
return ViewIndex(model);
}
private IActionResult ViewIndex(IndexModel model)
{
// Set airport select items:
model.FromAirports = DataStore.Airports
.Select(a => new SelectListItem(a, a, model.Flight.FromAirport == a));
model.ToAirports = DataStore.Airports
.Where(a => a != model.Flight.FromAirport) // exclude departure airport
.Select(a => new SelectListItem(a, a, model.Flight.ToAirport == a));
// Return view:
return View("Index", model);
}
请注意,我们已将该方法一分为二:Index操作方法和返回Index视图的ViewIndex方法。这样可以更轻松地从其他操作方法返回Index视图。
在ViewIndex方法中,当设置ToAirports时,我们排除了所选的FromAirport。这样,用户就无法选择相同的机场进行到达和离开。
但此代码仅在最初呈现视图时执行。现在,让我们使用页面部件呈现,当用户选择不同的出发机场时,也使用此代码“动态”更新视图。
首先,通过从 _Layout.cshtml 文件中引用CDN上的Sircl库文件(如第1部分中所述),或通过本页所述的任何方式,将Sircl库添加到项目中。
接下来,我们指定每当FromAirport更改时都需要刷新表单。这是通过将类onchange-submit添加到FromAirport SELECT控件来完成的。
如果表单的操作指向该Index操作,则将提交表单并重新呈现它。到目前为止,由于尚未指定内联目标,因此将使用整页请求。这意味着每次更改都会发生整页加载。我们可以通过指定目标来切换到页面部分请求,并让服务器在请求时返回部分视图:
- 向表单添加一个target类,使窗体成为部分页面请求的内联目标
- 将IndexView控制器方法的最后一行替换到以下代码中,以返回整页视图或部分视图:
// Return full view or partial view:
if (Request.Headers["X-Sircl-Request-Type"] == "Partial")
return PartialView("Index", model);
else
return View("Index", model);
第二步的替代方法是在 _Layout.cshtml 文件或 _ViewStart.cshtml 文件中添加代码,如本系列的第2部分所述。
为了获得最佳体验,我们可以通过sub-target属性将ToAirport控件定义为子目标。这将确保仅更新目的地机场选择控件的值列表。这是表单现在的样子(剥离了演示元素):
@model FlightModel
<form asp-action="Index" method="post" class="target">
<h2>Book your flight</h2>
<fieldset>
<legend>Flight</legend>
<div>
<label>Date:</label>
<input asp-for="Flight.Date" type="date">
</div>
<div>
<label>From:</label>
<select asp-for="Flight.FromAirport"
asp-items="Model.FromAirports" class="onchange-submit"
sub-target="#@Html.IdFor(m => m.Flight.ToAirport)">
<option value="">(Select an airport)</option>
</select>
</div>
<div>
<label>To:</label>
<select asp-for="Flight.ToAirport" asp-items="Model.ToAirports">
<option value="">(Select an airport)</option>
</select>
</div>
</fieldset>
</form>
请注意该sub-target属性如何使用以哈希(#)为前缀的Html.IdFor函数。该sub-target值是一个CSS选择器,CSS选择器引用以哈希id开头的元素。当然,如果您知道元素的id,你不需要使用IdFor函数。
添加乘客
添加乘客包括将PassengerModel对象添加到模型Passengers列表中。这将生成一组额外的FirstName和LastName字段供用户填写。因此,在服务器端,添加乘客的操作方法是:
public IActionResult AddPassenger(FlightModel model)
{
ModelState.Clear();
model.Flight.Passengers.Add(new());
return ViewIndex(model);
}
清除模型状态可确保不返回验证错误,但还可以确保列表中不显示过时的值。每当操作方法的目的是操作模型对象时,它就应该清除模型状态。
AddPassenger操作方法将一个(空白)passenger对象添加到passengers列表中并返回Index视图。
移除乘客
要删除passenger,我们需要知道要删除哪一个,因此我们希望给出index。删除操作方法为:
public IActionResult RemovePassenger(FlightModel model, int index)
{
ModelState.Clear();
model.Flight.Passengers.RemoveAt(index);
return ViewIndex(model);
}
在表单中,添加以下fieldset内容以呈现passenger列表,包括用于添加和删除乘客的按钮:
<fieldset>
<legend>Passengers</legend>
@for(int i=0; i<Model.Flight.Passengers.Count; i++)
{
<div
<span class="float-end">
<button type="submit"
formaction="@Url.Action("RemovePassenger", new { index = i })">
×
</button>
</span>
<p>Passenger @(i + 1): </p>
<div>
<div>
<input asp-for="Flight.Passengers[i].FirstName">
</div>
<div>
<input asp-for="Flight.Passengers[i].LastName">
</div>
</div>
</div>
}
<div>
<button type="submit" formaction="@Url.Action("AddPassenger")">
Add passenger
</button>
</div>
</fieldset>
由于表单具有目标类,因此所有表单提交操作都是返回整个表单(而不是整个页面)的部分页面请求。您可以添加一个sub-target以仅更新passengers字段集,甚至更小的部分。
提交表格
当用户填写表单时,他必须能够提交表单并转到“下一步”步骤。
如果我们添加常规提交按钮,则表单将使用页面部件请求提交到Index操作中。要覆盖该操作,我们只需将asp-action属性(或HTML formaction属性)设置为所需的操作(url)。
要覆盖内联目标,我们可以设置HTML formtarget属性。我们可以将其设置为CSS选择器以指向另一个内联目标,也可以将其设置为_self请求整页加载:
<button type="submit" asp-action="Next" formtarget="_self">
Next
</button>
您还可以在按钮上添加图标。例如,通过在_Layout.cshtml文件的HEAD部分中添加以下行来添加Boostrap图标:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
然后在按钮上添加一个图标,如下所示:
<button type="submit" asp-action="Next" formtarget="_self">
<i class="bi bi-caret-right-fill"></i>
Next
</button>
“下一个控制器”操作方法是我们验证窗体的位置。在这里,我们可以添加模型上的DataAnnotation属性不支持或未实现的其他验证规则:
[HttpPost]
public IActionResult Next(IndexModel model)
{
// Ensure flight is not in the past:
if (model.Flight.Date.HasValue &&
model.Flight.Date.Value < DateOnly.FromDateTime(DateTime.Now))
{
ModelState.AddModelError("Flight.Date", "Flight date cannot be in the past.");
}
// Ensure from and to airports are different:
if (model.Flight.FromAirport != null &&
model.Flight.FromAirport == model.Flight.ToAirport)
{
ModelState.AddModelError("Flight.ToAirport",
"Departure airport cannot be the same as destination airport.");
}
// Ensure at least one passenger is registered:
if (model.Flight.Passengers.Count == 0)
{
ModelState.AddModelError("", "There must be at least one passenger.");
}
// If valid, go next:
if (ModelState.IsValid)
{
return View("Next");
}
// Otherwise, return the index view (with validation errors):
return ViewIndex(model);
}
如果表单有效,则呈现Next视图。否则,将返回Index视图。此时,此视图还应显示验证错误。
对于每个字段,将具有该asp-validation-for属性的元素添加到相应的字段,如下所示:
<span asp-validation-for="Flight.Date" class="text-danger"></span>
此外,由于某些验证错误与特定字段无关,因此还应添加以下代码。将其放在H2标题的正下方:
<div asp-validation-summary="All" class="alert
alert-danger alert-dismissible fade show mb-3">
<strong>Following errors have occured:</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
要在没有验证错误时不显示此部分,请添加此小CSS。您可以在_Layout.cshtml文件中添加它,或者更好的是,在/wwwroot/css/site.css文件中添加它(当然没有style元素标记):
<style>
.validation-summary-valid {
display: none;
}
</style>
就是这样。我们现在有一个很好的、功能齐全的飞行登记表,完全基于C#代码和HTML。
注意:
我们现在有一个很好的、功能齐全的表单,完全基于C#代码和HTML。
变化检测
在这一点上,我们的表单是可操作的。用户可以输入表单并转到下一步。如果用户输入表单,然后由于分心或其他原因点击了错误的链接,例如“隐私”链接,则输入的数据将丢失。
Sircl中的更改检测允许一个人检测表单是否包含更改(并可能在提交时采取相应的行动),并允许两个人保护网页防止数据丢失。
表单中的更改可能来自两个来源:用户与控件交互INPUT或TEXTAREA控制(选中复选框、更改值)或服务器更改模型数据。以完整的航班登记表为例,用户单击以删除一名乘客。这也会更改数据。因此,Web客户端和服务器都能够启动更改。
此外,当由于验证错误而提交并返回已更改的表单时,表单必须保持更改状态,因为以前的更改未保存。
为了实现这一切,我们向IndexModel类添加了HasChanges属性(你可以以不同的方式命名它):
public class IndexModel
{
public bool HasChanges { get; set; }
...
}
在表单中,我们为此属性添加一个hidden字段。在form元素上,我们添加一个onchange-set属性,其中包含HasChanges属性的名称:
<form asp-action="Index" method="post" class="target"
onchange-set="HasChanges">
<input type="hidden" asp-for="HasChanges" />
...
</form>
每当表单发生更改时,Sircl都会将名为HasChanges的字段设置为值true。
在操作模型数据(AddPassenger和RemovePassenger)的控制器操作中,我们还将HasChanges属性设置为true。将以下行添加到这两个操作方法中:
model.HasChanges = true;
窗体现在将知道它是否包含更改,并且此信息通过HasChanges模型属性到达服务器。由于Sircl将form-changed类添加到处于更改状态的表单中,因此您可以使用CSS来设置表单或(某些)元素的样式。
但是,我们没有通过单击随机链接来保护表单不会丢失此更改。
对于最后一步,请添加一个带有消息的onunloadchanged-confirm属性,以向用户显示更改何时可能丢失:
<form asp-action="Index" method="post" class="target"
onchange-set="HasChanges" onunloadchanged-confirm="Ok to loose changes ?">
<input type="hidden" asp-for="HasChanges" />
...
</form>
现在,当用户更改表单,然后单击随机链接时,将询问确认。
在此页面上查找有关Sircl中更改状态管理的更多信息。
请注意,这并不能防止关闭浏览器窗口或使用浏览器功能(重新加载或后退按钮,或单击收藏夹)导航。为此,您需要在页面中实现onbeforeunload处理程序。
更多信息: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
Spinners
Sircl提供了几种方法来通知用户正在发生部分页面请求。这包括叠加层(页面部分上的半透明块)、进度条和微调器。让我们将微调器添加到各种表单按钮中。
要使用Sircl添加微调器,只需在按钮(或超链接)内添加一个带有spinner类的元素即可。在请求期间,微调器元件将被纺车取代。请求完成后,将恢复元素。
这是带有微调器的Add passenger按钮:
<button type="submit" formaction="@Url.Action("AddPassenger")">
<span class="spinner"></span>
Add passenger
</button>
为了在删除passenger的同时用微调器替换十字(x),我们将十字放在spinner元素内。旋转时,整个元件被spinning wheel取代:
<button type="submit"
formaction="@Url.Action("RemovePassenger", new { index = i })">
<span class="spinner">×</span>
</button>
对于“下一步”按钮,我们可以将spinner类添加到图标中,以便将图标替换为spinning wheel。但这里还有一个问题。由于Sircl只能在执行Ajax调用时恢复微调器,因此默认情况下不会在执行常规(浏览器控制)导航的元素上启用微调器和其他功能。若要在常规导航中也选择加入这些功能,请将onnavigate类添加到触发元素(BUTTON)或其任何父元素(包括BODY元素):
<button type="submit" asp-action="Next" class="onnavigate" formtarget="_self">
<i class="bi bi-caret-right-fill spinner"></i>
Next
</button>
默认情况下,Sircl使用与默认Web样式匹配的CSS动画构建一个spinner。如果您使用的是Bootstrap,您会发现使用Bootstrap微调器是更好的选择。如果包含Sircl引导库,则Sircl会自动使用Bootstrap微调器,例如,通过向_Layout.cshtml文件添加以下行(在添加sircl.min.js文件后):
<script src="https://cdn.jsdelivr.net/npm/sircl@2.3.7/sircl-bootstrap5.min.js"></script>
如果您使用的是Font Awesome,您可以(也)包含Font Awesome Sircl扩展名,它基本上除了覆盖spinner元素之外什么都不做:
<script src="https://cdn.jsdelivr.net/npm/sircl@2.3.7/sircl-fa.min.js"></script>
顺便说一句,在本文的示例代码中,我为各种控制器操作添加了延迟,以便您有时间查看微调器的运行情况。
避免重新提交
微调器是确认用户操作的好方法。如果没有它,如果请求花费的时间太长,用户可能会变得不耐烦并多次按下按钮,从而发出多个请求,并可能淹没已经过于繁忙的服务器。
但是微调器并不能阻止用户多次点击它。而且我看到用户(包括我的年幼的孩子)多次按下按钮,期望事情会进展得更快......
使用Sircl,避免多次提交表单只需在表单上添加一个onsubmit-disable类即可。当提交请求挂起时,这将禁用所有提交控件。简单有效!
<form asp-action="Index" method="post" class="target onsubmit-disable"
onchange-set="HasChanges" onunloadchanged-confirm="Ok to loose changes ?">
...
</form>
键盘支持
对于某些(Web)表单,键盘支持很重要。对于其他人来说,它往往没有得到足够的关注。在我们目前的表单中,键盘确实存在一个问题:在Date字段中按ENTER键将删除第一位乘客或添加一名乘客!
要理解原因,我们必须了解HTML表单如何处理ENTER键。默认情况下,ENTER使用第一个提交按钮提交表单。如果未找到提交按钮,则使用表单操作URL或当前页面URL提交表单。
在我们的例子中,我们希望触发“下一个提交”按钮,它不是表单中的第一个提交按钮。
要使用Sircl覆盖默认按钮,请使用所需按钮的CSS选择器向表单添加onkeyenter-click属性。例如,向“下一步”按钮添加一个id属性,并在form元素上添加一个引用该id的onkeyenter-click属性:
<form asp-action="Index" method="post" class="target onsubmit-disable"
onchange-set="HasChanges" onunloadchanged-confirm="Ok to loose changes ?"
onkeyenter-click="#nextbtn">
...
<button id="nextbtn" type="submit" asp-action="Next"
class="onnavigate" formtarget="_self">
<i class="bi bi-caret-right-fill spinner"></i>
Next
</button>
</form>
在随附的代码中,您还将看到我如何使用该autofocus属性将焦点设置在新添加的客运线路的“名字”字段上。这也便于键盘输入。
结论
在本文中,我们展示了如何通过将ASP.NET Core(MVC)与Sircl相结合,我们可以使用专门(命令式)C#代码和(声明式)HTML创建交互式Web表单和应用程序。我们还看到Sircl包含轻松解决常见Web表单问题的功能。
在下一篇文章中,我们将看到如何使用Bootstrap模态(或HTML 5对话框)来进一步增强我们的Web应用程序,以及Sircl如何使它们易于使用。
同时,在 Sircl - Interactive web apps made easy 上找到有关Sircl的所有信息。
https://www.codeproject.com/Articles/5373198/Build-Rich-Web-Apps-with-ASP-NET-Core-and-Sircl-Pa