65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2024 年 4 月 10 日

CPOL

7分钟阅读

viewsIcon

5943

downloadIcon

121

在本系列中,我们将介绍如何使用 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">&times;</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元素中。这有三个结果:

  1. HTML代码在客户端可用,无需往返服务器获取
     
  2. TEMPLATE元素内的HTML代码不会被渲染
     
  3. 即使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">&times;</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">&times;</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部分的版本进行比较:看看AddPassengerRemovePassenger操作方法是如何消失的,以及代码是如何更改以适应重构后的乘客列表数据的。

 

© . All rights reserved.