目录
介绍
在本系列中,我们将了解如何仅使用ASP.NET Core和Sircl轻松构建出色的交互式Web应用程序(通常需要大量JavaScript代码或用JavaScript框架编写的应用程序。
Sircl是一个开源的客户端库,它扩展了HTML以提供部分更新和常见行为,并可以轻松编写依赖于服务器端渲染的丰富应用程序。
在本系列的每一部分中,我们将介绍使用服务器端技术的富Web应用程序的典型“编程问题”,并了解如何使用Sircl在ASP.NET Core中解决这个问题。
在上一篇文章中,我们了解了Event-Actions作为在网页中声明客户端行为的一种方式。今天,我们将了解Sircl的另一个基石:部分页面加载。
- 使用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部分)
动态行为
正如我们在上一部分所看到的,使用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创建小组件,例如,可为null的Yes/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']">×</span>
例如,您可以将其转换为可为null的布尔值的ASP.NET编辑器模板。但稍后会详细介绍使用Sircl编写编辑器和组件。
但是,有时客户端Event-Actions是不够的。事件操作可能不足的原因有两个:
- 交互太复杂了。例如,Event-Actions不能管理复杂的“和”/“或”情况。
- 需要查询(Web)服务或数据库以确定如何更新UI。
以国际地址输入表为例,用户需要选择国家/地区(如果适用)州。是否必须选择州取决于国家/地区(对于美国和加拿大,需要注明州,但大多数其他国家/地区并非如此),当然,可供选择的州列表也取决于所选国家/地区:
注意:
只要没有选择国家/地区,系统就不知道是否需要某个州,以及用户应该从哪些州中进行选择......
我见过使用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 & 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标准定义了formenctype、formmethod、formnovalidate和formtarget属性以覆盖表单的默认值。
我们现在有一个工作版本:用户可以输入地址。每当用户更改国家/地区时,表单都会发布并使用正确的状态列表重新呈现。除了切换表单和按钮上的操作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有几种方法可以做到这一点,让我们看看两个选项:
选项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 StateListContainer的DIV元素中。这是因为我们需要参考它:
<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将尊重国家控制的formaction和formtarget属性。因此,它会将表单发布到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 & 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