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

目录

介绍

动态行为

使用Sircl重新呈现表单

页面部分请求简介

使用页面部分请求重新呈现

选项1——仅更新状态列表

选项2——更新整个表单

结论


介绍

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

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

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

上一篇文章中,我们了解了Event-Actions作为在网页中声明客户端行为的一种方式。今天,我们将了解Sircl的另一个基石:部分页面加载。

动态行为

正如我们在上一部分所看到的,使用Event-Actions的动态行为是一个强大的概念,可以处理简单而频繁的交互。

例如,一组复选框,其中第二个复选框只有在选择第一个复选框时才有意义。选择进行视频会议,并选择录制视频会议。要录制它,它必须是视频会议:

<label>
   <input type="checkbox" name="videoconf" ifunchecked-uncheck="input[name='record']">
   Video conference
</label>
<label>
   <input type="checkbox" name="record" ifchecked-check="input[name='videoconf']">
   Record the conference
</label>

您可以基于Event-Actions创建小组件,例如,可为nullYes/No字段:

<label>
   <input type="radio" name="MyBoolean" value="True"> Yes
</label>
<label>
   <input type="radio" name="MyBoolean" value="False"> No
</label>
<span onclick-uncheck="input[name='MyBoolean']">&times;</span>

例如,您可以将其转换为可为null的布尔值的ASP.NET编辑器模板。但稍后会详细介绍使用Sircl编写编辑器和组件。

但是,有时客户端Event-Actions是不够的。事件操作可能不足的原因有两个:

  1. 交互太复杂了。例如,Event-Actions不能管理复杂的”/“情况。
  2. 需要查询(Web)服务或数据库以确定如何更新UI

以国际地址输入表为例,用户需要选择国家/地区(如果适用)州。是否必须选择州取决于国家/地区(对于美国和加拿大,需要注明州,但大多数其他国家/地区并非如此),当然,可供选择的州列表也取决于所选国家/地区:

https://www.codeproject.com/KB/Articles/5372446/AddressForm1.png?r=590fd04f-97cc-4ed7-93fb-c96df583314a

注意:

只要没有选择国家/地区,系统就不知道是否需要某个州,以及用户应该从哪些州中进行选择......

我见过使用JavaScript的解决方案,其中所有国家/地区的所有可能状态都在页面中硬编码并下载到客户端。这显然不是一个理想的解决办法,也不是在所有情况下都行得通。有时数据量太大,或者您不想公开它。或者,服务器需要结合复杂的服务器端逻辑和数据做出决策。

因此,让我们看看如何使用Sircl解决这个问题。

使用Sircl重新呈现表单

下面可能是上面所示的地址输入表单的Razor视图(为了便于阅读,删除了样式元素):

@model AddressFormModel

<form method="post" asp-action="Next">
    <fieldset>
        <legend>Address</legend>
        <div>
            <label>Name: *</label>
            <input type="text" asp-for="Address.Name">
        </div>
        <div>
            <label>Street &amp; Number: *</label>
            <input type="text" asp-for="Address.Street">
        </div>
        <div>
            <label>City: *</label>
            <input type="text" asp-for="Address.City">
        </div>
        <div>
            <label>Country: *</label>
            <select asp-for="Address.CountryCode" asp-items="Model.Countries">
                <option value="">(select a country)</option>
            </select>
        </div>
        @if (Model.States.Any())
        {
            <div>
                <label>State: *</label>
                <select asp-for="Address.StateCode" asp-items="Model.States">
                    <option value="">(select a state)</option>
                </select>
            </div>
        }
    </fieldset>

    <button type="submit">Next</button>

</form>

StateCode字段仅针对有州的国家/地区显示,因此是Razor IF指令。但问题是用户可以在表单上更改国家/地区。然后,我们需要重新渲染表单,以执行Razor IF语句。为此,我们需要将表单回发到服务器:我们需要提交表单。

要在国家/地区更改时自动提交表单,我们可以使用onchange-submit Event-Action,它只是在CountryCode SELECT元素上添加的一个类:

<select class="onchange-submit" asp-for="Address.CountryCode" asp-items="Model.Countries">
    <option value="">(select a country)</option>
</select>

这将提交具有默认操作的表单,即在FORM元素上定义的操作。因此,我们必须将默认操作更改为索引(或最初呈现表单的任何操作),并将下一步操作移动到下一步按钮,如下所示:

<form method="post" asp-action="Index">
    ...
    <button type="submit" asp-action="Next">Next</button>
</form>

这是一个不太常见的设计,但如果你仔细想想,它是有道理的:让窗体自己呈现,让按钮定义它们采取的操作。如果您对不同的操作有不同的按钮,那么在按钮本身上添加操作会很自然。

asp-action属性在放置在FORM元素上时转换为 action 属性,当放置在按钮上时转换为 formaction 属性。formaction 属性是一个标准HTML属性,它允许submit元素覆盖表单的默认提交操作。同样,HTML标准定义了formenctypeformmethodformnovalidateformtarget属性以覆盖表单的默认值。

我们现在有一个工作版本:用户可以输入地址。每当用户更改国家/地区时,表单都会发布并使用正确的状态列表重新呈现。除了切换表单和按钮上的操作URL外,我们只需要添加一个onchange-submit Event-Action

以下是Index操作的控制器代码:

public IActionResult Index(AddressFormModel model)
{
    model.Countries = DataStore.Countries
        .Select(c => new SelectListItem(c.Name, c.Code, 
                c.Code == model.Address.CountryCode));
    model.States = DataStore.States
        .Where(s => s.CountryCode == model.Address.CountryCode)
        .Select(s => new SelectListItem(s.Name, s.Code, 
                s.Code == model.Address.StateCode));
            
    return View(model);
}

第一个解决方案的完整代码可以在这里下载:

但是这个解决方案并不完美:每当国家/地区更改时,都会提交表单,因此浏览器会执行整个页面加载。这也使得表单只能从键盘上使用。让我们看看我们是否能做得更好。

页面部分请求简介

Sircl提供了请求服务器更新部分页面(或表单的一部分)的功能。我们可以在这里使用它来更新状态列表,以便通过对服务器的调用进行选择。

Sircl基本上提供的是一种执行Ajax调用的方法,并让该Ajax调用返回HTM代码以替换(或扩展)当前页面的一部分。本质上允许我们创建一个简单的单页应用程序或SPA

每当要检索URL或发布表单时,Sircl会自动创建这样的Ajax页面部件请求,并且链接或表单具有本地目标

让我们看一个常规的超链接:

<a href="/SomePage">Go to some page</a>

这里没什么特别的:单击链接将使浏览器导航到该页面并显示一个新页面。

现在让我们添加一个target

<a href="/SomePage" target="_blank">Go to some page</a>

同样在这里,浏览器将导航到该页面并呈现一个新页面。

但是,当目标值是页面中某个位置的CSS选择器时,就会发生不同的事情:

<a href="/SomePage" target="#here">Go to some page</a>
<div id="here"></div>

在这种情况下,Sircl将拦截超链接,发出页面部分请求以使用Ajax加载/SomePage,并将响应HTML放在#here DIV元素中。简而言之:它将替换页面的一部分。

注意:

简而言之:它将替换页面的一部分。

这同样适用于表单:

<form class="target" action="/SomeAction" method="post">
   Name: <input type="text" name="username" />
  <button type="submit">OK</button>
</form>

在这里,target属性被一个target类替换。添加target类与使用与当前元素(此处的FORM元素)匹配的CSS选择器放置target属性相同。在这种情况下,提交表单会将FORM元素的内容替换为来自服务器的响应。

在服务器端,处理页面部件请求只需要将视图返回为PartialView或将_Layout属性设置为null,以便不返回布局模板。

有关使用Sircl进行部分页面加载的更多信息:

Sircl - Partial Loading

使用页面部分请求重新呈现

因此,让我们更新我们的地址输入表单以使用页面部件请求。使用Sircl有几种方法可以做到这一点,让我们看看两个选项:

选项1——仅更新状态列表

在此解决方案中,我们将仅更新州列表,并且仅在国家/地区更改时更新。

因此,我们需要一个操作方法(我在这里使用MVC),我们可以调用它来获取状态列表(要获取整个选择控件,我的意思是,而不仅仅是列表的内容,因为选择控件是否存在也取决于所选国家/地区):

[HttpPost]
public IActionResult StateList(AddressFormModel model)
{
    model.States = DataStore.States
        .Where(s => s.CountryCode == model.Address.CountryCode)
        .Select(s => new SelectListItem(s.Name, s.Code, s.Code == model.Address.StateCode));

    return PartialView(model);
}

StateList操作与其他表单处理操作具有相同的模型参数,因为整个表单将提交给它。这可确保StateList操作可以访问表单中的所有数据以做出决定。

StateList操作将当前国家/地区的状态列表放在模型的States属性中,并确保选择当前状态(如果适用)。

然后,该StateList操作返回视图,但重要的是,它返回它为PartialView!通过网络,我们只想返回选择控件,而不是带有页眉、导航栏和页脚的整个页面。

若要创建StateList.cshtml视图,请将包含Razor IF语句的StateCode字段移动到单独的视图:

@model AddressFormModel

@if (Model.States.Any())
{
    <div>
        <label>State: *</label>
        <select asp-for="Address.StateCode" asp-items="Model.States">
            <option value="">(select a state)</option>
        </select>
    </div>
}

在原始的Index.cshtml视图中,我们删除了该StateCode字段(包括其周围的IF字段),但将其替换为对StateList视图的调用。毕竟,我们可能编辑了一个已经填写的地址,该地址设置了状态。在这里,我们已将对分部视图的调用包含在具有id StateListContainerDIV元素中。这是因为我们需要参考它:

<div id="StateListContainer">
    <partial name="StateList" />
</div>

最后,我们需要定义当国家/地区发生变化时,我们想要更新州列表。为此,我们让countries上的onchange-submit类控制、覆盖formaction并指定指向元素的formtarget属性以包含状态列表控件:

<select class="onchange-submit" asp-for="Address.CountryCode" asp-items="Model.Countries"
        formaction="@Url.Action("StateList")" formtarget="#StateListContainer">
    <option value="">(select a country)</option>
</select>

更改国家/地区时,onchange-submit将触发表单提交。由于国家控制是触发因素,Sircl将尊重国家控制的formactionformtarget属性。因此,它会将表单发布到StateList操作中,并将返回的HTML放在带有StateListContainer id的元素中。

请注意,任何”HTML元素上对 formaction  formtarget(以及其他form*属性)的支持是Sircl扩展。与Sircl中一样,任何元素都可以触发表单提交,任何元素都可以具有form*属性。但是,只有当Sircl处理表单提交时,即Ajax调用时,他们才会得到尊重。像我们的第一个解决方案一样,整页调用由浏览器处理,浏览器仅在提交元素上支持这些属性。

第二种解决方案的完整代码可以在这里下载:

选项2——更新整个表单

解决该问题的另一种方法是,当需要重新呈现窗体时,让整个窗体重新呈现(不仅仅是StateCode控件),而只是呈现窗体(而不是整个页面)。

这种方法的优点是我们不需要拆分视图,并且有多种操作方法。

若要应用此解决方案,CountryCode控件仍需要具有onchange-submit类,因为其值的更改仍是提交表单的触发器。它也有一个formtarget,但现在应该指向该FORM元素。它不需要具有formaction属性,因为它不需要覆盖表单的默认操作:

<select class="onchange-submit" asp-for="Address.CountryCode" asp-items="Model.Countries"
        formtarget="#AddressForm">
    <option value="">(select a country)</option>
</select>

所以表单元素必须得到id="AddressForm"

这样,更改国家/地区将使用Ajax调用将整个表单提交给索引操作。

对索引操作的正常调用,如导航到地址输入表单时,应返回整个页面(表单以及页脚、页眉、导航栏等)。然而,Sircl发出的Ajax调用应该只返回表单。因此,需要对服务器端进行区分。

每当Sircl发起Ajax调用时,它都会将X-Sircl-Request-Type请求标头设置为Partial

现在,Index操作不仅要返回视图,还必须决定是将视图作为常规视图(被布局模板包围)还是作为部分视图返回:

if (Request.Headers["X-Sircl-Request-Type"] == "Partial")
    return PartialView(model);
else
    return View(model);

或者,可以在_Layout.cshtml模板之上添加以下代码:

@if (this.Context.Request.Headers["X-Sircl-Request-Type"] == "Partial")
{
    @:@RenderBody()
    return;
}

如果将其放在_Layout.cshtml模板中,则始终可以从控制器方法进行return View(model);,因为周围的布局模板将决定是否呈现。或者将_Layout.cshtml保留原样,但更改_ViewStart.cshtml中的代码,为Sircl Ajax的调用将Layout变量设置为null

但是,此解决方案存在一个问题:由于替换了整个窗体,包括具有焦点的控件,因此焦点丢失了。这很烦人,特别是对于使用键盘进行数据输入的用户。

幸运的是,有一个小修复:我们将使用sub-target!该sub-target属性是一个Sircl扩展,允许指定要替换目标的哪些部分。所以目标仍然是整个表单,服务器仍然会返回整个表单的HTML代码,但只会替换子目标元素。

sub-target属性可以引用具有多个匹配项的类,以便可以在不更新整个表单的情况下更新多个元素。

通过用可识别的元素包围StateCode控件(包括其Razor IF指令),并在CountryCode控件(触发器元素)上设置指向该元素的sub-target属性,我们可以保持服务器端代码的简单性,但只替换StateCode控件,从而保持焦点。

整个地址输入表单代码现在是(不带样式元素):

@model AddressFormModel

<form id="AddressForm" method="post" asp-action="Index">
    <fieldset>
        <legend>Address</legend>
        <div>
            <label>Name: *</label>
            <input type="text" asp-for="Address.Name">
        </div>
        <div>
            <label>Street &amp; Number: *</label>
            <input type="text" asp-for="Address.Street">
        </div>
        <div>
            <label>City: *</label>
            <input type="text" asp-for="Address.City">
        </div>
        <div>
            <label>Country: *</label>
            <select class="onchange-submit" asp-for="Address.CountryCode" asp-items="Model.Countries"
                        formtarget="#AddressForm" sub-target="#StateListContainer">
                <option value="">(select a country)</option>
            </select>
        </div>
        <div id="StateListContainer">
            @if (Model.States.Any())
            {
                <div>
                    <label>State: *</label>
                    <select asp-for="Address.StateCode" asp-items="Model.States">
                        <option value="">(select a state)</option>
                    </select>
                </div>
            }
        </div>
    </fieldset>

    <button type="submit" asp-action="Next">Next</button>

</form>

Index控制器操作为:

public IActionResult Index(AddressFormModel model)
{
    // Load data for Countries and States list:
    model.Countries = DataStore.Countries
        .Select(c => new SelectListItem(c.Name, c.Code, 
                c.Code == model.Address.CountryCode));
    model.States = DataStore.States
        .Where(s => s.CountryCode == model.Address.CountryCode)
        .Select(s => new SelectListItem(s.Name, s.Code, 
                s.Code == model.Address.StateCode));

    // Return full view or partial view:
    if (Request.Headers["X-Sircl-Request-Type"] == "Partial")
        return PartialView(model);
    else
        return View(model);
}

(如您所见,控制器代码不知道子目标的使用情况。

我们最终执行与选项1相同的操作:仅更新状态列表。但是这一次,我们不需要为它创建第二个控制器操作方法,也不需要拆分我们的Razor视图。对我们原始代码的影响微乎其微。

第三种解决方案的完整代码可以在这里下载:

结论

同样,我们已经看到了Sircl如何允许我们编写动态应用程序——这次使用服务器端渲染——通过添加声明性HTML属性和类而不是编写命令式JavaScript代码,对代码的影响最小。

虽然在本文中,我们举了一个简单的例子,但在现实世界中,许多复杂的形式都可以以这种方式实现。下次,我们将进一步讨论这个问题,看看如何使用表单和Sircl管理项目列表。

同时,在以下位置找到有关Sircl的所有信息:

Sircl - Interactive web apps made easy

https://www.codeproject.com/Articles/5372446/Build-Rich-Web-Apps-with-ASP-NET-Core-and-Sircl-2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值