使用ASP.NET Core和Sircl构建富Web应用程序 – 第3部分 (b)





5.00/5 (1投票)
在本系列中,我们将介绍如何使用 Sircl 在 ASP.NET Core 中构建交互式 Web 应用程序。
引言
在第3部分中,我们了解了如何更新包含列表的页面,以及如何向其中添加或删除元素。我们基本上有一个操作方法用于向列表中添加项目(AddPassenger
),以及一个操作方法用于从列表中删除项目(RemovePassenger
)。
这两个操作方法都需要往返服务器,重新渲染列表,其中包含一组额外的字段用于新乘客,或删除一组字段。
在本第3b部分文章中,我们将看到如何在不往返服务器的情况下实现类似的行为。为此,您需要至少2.4.0版本的Sircl。如果您从之前的代码开始,请确保将Sircl升级到此版本或最新版本。
限制
有一些限制需要考虑。主要的一点是,除了不同列表元素上的用户输入之外,不应有其他动态内容。
我们在这里有一个关于原始航班注册表单的问题:列表显示乘客编号(1、2、3...)。这是由服务器计算的动态内容。如果取消往返服务器,我们也取消了渲染服务器计算内容的能力。因此,我们将用“乘客”替换“乘客1”标签。
我们遇到的另一个问题与ASP.NET (MVC) 模型绑定有关。绑定到列表中对象的属性需要字段名包含元素的索引(0、1、2...),这再次是由服务器计算的动态内容。
模型绑定的确切行为和语法取决于您使用的服务器端技术,并且不在Sircl的范围之内。但这是一个值得注意的点。
在我们的例子中,我将乘客列表从一个Passenger对象列表重构为两个字符串列表(一个列表用于名字,一个用于姓氏)。这并不理想,但这并非本文的主题...
在讨论了限制之后,现在让我们关注解决方案。
移除项目
在HTML中,从列表中移除项目(在本例中是从航班中移除乘客)非常简单:只需剪切代表该列表项的HTML代码。为了响应点击事件移除HTML,我们使用Sircl的onclick-remove
事件-动作属性。属性值是一个CSS选择器,指向要移除的元素。
现在,鉴于所有列表条目都具有相同的HTML,如何识别要移除的元素呢?为每个条目赋予唯一的ID将再次需要服务器端计算。
解决方案在于使用_相对CSS选择器_。这是Sircl的补充。它基本上允许通过元素相对于引用它们的位置来选择元素。并且这在Sircl中很有意义,Sircl是一个使用内联CSS选择器的HTML扩展。
使用相对CSS选择器,用于移除与当前乘客条目匹配的HTML代码的按钮可以引用它所属的父列表条目元素。
考虑以下表示列表中乘客条目的代码
<div class="mb-3">
<span class="float-end">
<button type="button" class="btn btn-sm btn-light onclick-setchanged" onclick-remove="<DIV" title="Remove this passenger">×</button>
</span>
<p><b>Passenger:</b></p>
<div class="row">
<div class="col">
<input name="Flight.PassengerFirstNames" value="" class="form-control" placeholder="First name" required>
</div>
<div class="col">
<input name="Flight.PassengerLastNames" value="" class="form-control" placeholder="Last name" required>
</div>
</div>
</div>
onclick-remove
属性的值是“<DIV
”。这是一个相对CSS选择器:“<
”箭头表示从当前元素向上查找与其余表达式最接近的匹配项。换句话说,这匹配最接近的父DIV
元素。当用户点击“x”按钮时,这就是要移除的元素。
更改检测
在上一篇文章(第3部分)中,我们添加了一个更改检测功能,允许我们的表单知道其数据何时发生更改。它依赖于change事件以及服务器在模型上设置HasChanges
属性。
但是通过点击按钮移除HTML不会触发表单上的change事件。而且我们刚刚取消了服务器往返。然而,表单已经更改:少了一个乘客。
然而,这很容易解决:为了让表单在点击“x”按钮时意识到更改,我们可以在按钮上添加onclick-setchanged
事件-动作类。
添加项目
在不往返服务器的情况下向列表中添加项目稍微复杂一些。当要向列表中添加乘客时,需要将HTML代码添加到表单中。如果这些HTML代码不是来自服务器,那它将来自哪里呢?
为了解决这个问题,我们可以让服务器提前准备HTML代码,并在表单的初始渲染时将其包含在内。我们可以将代码放在TEMPLATE
元素中。这有三个结果:
- HTML代码在客户端可用,无需往返服务器获取
TEMPLATE
元素内的HTML代码不会被渲染
- 即使HTML代码在
FORM
元素内部并可能包含必填字段(或其他验证限制),但由于其在TEMPLATE
元素内部,其验证属性将被忽略
因此,我们可以安全地使用一个TEMPLATE
元素,并且该元素在表单内部或外部都无关紧要。这很好,因为这意味着我们可以将模板放置在它将要使用的地方旁边。
在下面的代码中,模板紧跟在引用它的按钮之后。
但是现在,我们如何使用模板将乘客添加到列表中呢?
我们仍然有一个添加乘客的按钮。只是这一次,该按钮不是导致服务器往返的提交按钮,而是一个仅包含 Sircl 事件-操作的普通按钮。您需要在该按钮上设置一个事件-操作,指示将模板内容附加到乘客列表。因此,我们需要引用(即使用(相对或绝对)CSS 选择器)模板以及列表。不幸的是,HTML 属性只能有一个值,为了简单起见,Sircl 遵守了这一限制。
所以我们需要一些技巧...
如果我们将事件-动作属性放在模板本身上,我们只需要引用列表,因为模板已经已知:它是当前元素。因此,我们在模板上添加onclick-appendto="#passengerlist"
属性。这表示将模板的内容附加到ID为“passengerlist
”的元素的内容中。
但TEMPLATE
元素是隐藏的,无法点击。然而,这并不意味着模板不能处理点击事件。所以现在我们只需要向模板发送一个点击事件:我们只需要引用一个项目:模板。现在我们可以在按钮上使用onclick-click="#passengertemplate"
事件-动作属性。这告诉按钮,当它被点击时,它应该触发ID为“passengertemplate
”的元素(即模板)上的点击事件。
我们知道,当模板收到点击事件时,它会将其内容附加到乘客列表的内容中。
我们所做的被称为_链接事件-动作_,它通常涉及点击事件,因为大多数Sircl动作都可以通过点击事件触发。
此外,在“添加”按钮上,我们还添加了onclick-setchanged
类,以便表单知道它已更改。
最后但同样重要的是,添加乘客时,我们可以将焦点设置在新添加行的“名字”字段上。这可以通过在“添加乘客”按钮上使用onclick-focus
属性来实现,给定一个CSS选择器来选择最后一个乘客的“名字”字段,例如:
onclick-focus="#passengerlist > *:last-child INPUT[name='Flight.PassengerFirstNames']"
但有一个更简单的方法:在模板的“名字”字段上添加autofocus
属性。每当模板被复制时,它的autofocus
就会被触发。
乘客列表现在看起来像
<fieldset>
<legend>Passengers</legend>
<div id="passengerlist">
@for (int i = 0; i < Model.Flight.PassengerFirstNames.Count; i++)
{
<div class="mb-3">
<span class="float-end">
<button type="button" class="btn btn-sm btn-light onclick-setchanged" onclick-remove="<DIV" title="Remove this passenger">×</button>
</span>
<p><b>Passenger:</b></p>
<div class="row">
<div class="col">
<input name="Flight.PassengerFirstNames" value="@(Model.Flight.PassengerFirstNames[i])" class="form-control" placeholder="First name" required>
</div>
<div class="col">
<input name="Flight.PassengerLastNames" value="@(Model.Flight.PassengerLastNames[i])" class="form-control" placeholder="Last name" required>
</div>
</div>
</div>
}
</div>
<div class="mb-3">
<button type="button" class="btn btn-sm btn-secondary onclick-setchanged" onclick-click="#passengertemplate">
Add passenger
</button>
</div>
<template id="passengertemplate" onclick-appendto="#passengerlist" onload-copyto="@(Model.Flight == null ? "#passengerlist" : null)">
<div class="mb-3">
<span class="float-end">
<button type="button" class="btn btn-sm btn-light onclick-setchanged" onclick-remove="<DIV" title="Remove this passenger">×</button>
</span>
<p><b>Passenger:</b></p>
<div class="row">
<div class="col">
<input name="Flight.PassengerFirstNames" value="" class="form-control" placeholder="First name" required autofocus>
</div>
<div class="col">
<input name="Flight.PassengerLastNames" value="" class="form-control" placeholder="Last name" required>
</div>
</div>
</div>
</template>
</fieldset>
客户端验证
限制服务器往返的另一种方法是尽可能实现客户端验证。在此示例中,我使用了HTML5表单验证的required
属性,以确保如果必填字段为空,则不会发生服务器往返。
服务器端验证仍然存在,例如,它将确保预订至少有一名乘客。
控制器
此版本的完整源代码可以下载。查看HomeController
并与第3部分的版本进行比较:看看AddPassenger
和RemovePassenger
操作方法是如何消失的,以及代码是如何更改以适应重构后的乘客列表数据的。