使用ASP.NET Core和Sircl构建丰富的Web应用程序——第3部分

目录

介绍

列表管理

添加乘客

移除乘客

提交表格

变化检测

Spinners

避免重新提交

键盘支持

结论


介绍

在本系列中,我们将了解如何仅使用ASP.NET CoreSircl轻松构建出色的交互式Web应用程序(通常需要大量JavaScript代码或用JavaScript框架编写的应用程序。

Sircl是一个开源的客户端库,它扩展了HTML以提供部分更新和常见行为,并可以轻松编写依赖于服务器端渲染的丰富应用程序。

在本系列的每一部分中,我们将介绍使用服务器端技术的富Web应用程序的典型编程问题,并了解如何使用SirclASP.NET Core中解决这个问题。

上一篇文章中,我们了解了如何将动态行为与页面部分请求相结合,以构建丰富且动态的网页。我们将继续添加列表管理,并发现一些特定于表单的功能。

列表管理

今天,我们将构建一个简单的航班登记表:用户选择航班(日期和机场)并输入一名或多名乘客。

由于事先不知道乘客人数,因此该表单提供了一种添加(和删除)乘客的方法。

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 属性才能正确设置日期格式,以便与类型为dateINPUT元素一起使用。

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操作,则将提交表单并重新呈现它。到目前为止,由于尚未指定内联目标,因此将使用整页请求。这意味着每次更改都会发生整页加载。我们可以通过指定目标来切换到页面部分请求,并让服务器在请求时返回部分视图:

  1. 向表单添加一个target类,使窗体成为部分页面请求的内联目标
  2. 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列表中。这将生成一组额外的FirstNameLastName字段供用户填写。因此,在服务器端,添加乘客的操作方法是:

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 })">
                  &times;
                </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中的更改检测允许一个人检测表单是否包含更改(并可能在提交时采取相应的行动),并允许两个人保护网页防止数据丢失。

表单中的更改可能来自两个来源:用户与控件交互INPUTTEXTAREA控制(选中复选框、更改值)或服务器更改模型数据。以完整的航班登记表为例,用户单击以删除一名乘客。这也会更改数据。因此,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

在操作模型数据(AddPassengerRemovePassenger)的控制器操作中,我们还将HasChanges属性设置为true。将以下行添加到这两个操作方法中:

model.HasChanges = true;

窗体现在将知道它是否包含更改,并且此信息通过HasChanges模型属性到达服务器。由于Sirclform-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">&times;</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元素上添加一个引用该idonkeyenter-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 CoreMVC)与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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值