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





5.00/5 (2投票s)
在本系列中,我们将介绍如何使用 Sircl 在 ASP.NET Core 中构建交互式 Web 应用程序。
引言
在本系列文章中,我们将展示如何仅使用 ASP.NET Core 和 Sircl,轻松构建出通常需要大量 JavaScript 代码或使用 JavaScript 框架编写的、交互式的 Web 应用程序。
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 部分
Bootstrap 模态框(第 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)创建小的组件,例如,一个可空的“是/否”字段。
<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>
您可以将此转换为 ASP.NET 编辑器模板,例如用于可空的布尔值。但关于使用 Sircl 编写编辑器和组件的内容,我们稍后会讲。
然而,有时客户端事件操作(Event-Actions)是不够的。事件操作(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
元素上定义的操作。所以我们必须将默认操作更改为“Index”(或最初渲染表单的任何操作),并将“Next”操作移动到“Next”按钮上,如下所示:
<form method="post" asp-action="Index">
...
<button type="submit" asp-action="Next">Next</button>
</form>
这是一种不那么常见的设计,但如果您仔细考虑,这是有道理的:让表单自行渲染,让按钮定义它们执行的操作。如果您有不同的按钮执行不同的操作,那么将操作添加到按钮本身感觉很自然。
当 asp-action
属性放在 FORM 元素上时,它会被翻译成 action
属性;当放在按钮上时,它会被翻译成 formaction
属性。formaction
属性是一个标准的 HTML 属性,它允许提交元素覆盖表单的默认提交操作。同样,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);
}
第一个解决方案的完整代码可在此处下载。
但这个解决方案并不完美:每当国家发生变化时,表单都会被提交,因此浏览器会执行完整的页面加载。这也使得表单无法仅通过键盘使用。让我们看看是否能做得更好。
引入页面部分请求(Page Part Requests)
Sircl 提供了请求服务器更新页面某一部分(或表单某一部分)的功能。我们可以用它来通过服务器调用更新省/州列表。
Sirclbasically 提供了一种执行 Ajax 调用并使该 Ajax 调用返回 HTML 代码以替换(或扩展)当前页面一部分的方法。本质上允许我们创建一个简单的单页应用程序(SPA)。
当需要检索 URL 或提交表单,并且链接或表单具有“局部目标”(local target)时,Sircl 会自动创建一个这样的 Ajax 页面部分请求。
我们来看一个常规的超链接。
<a href="/SomePage">Go to some page</a>
这没什么特别的:单击链接将使浏览器导航到页面并显示一个新页面。
现在让我们添加一个 target
。
<a href="/SomePage" target="_blank">Go to some page</a>
同样,浏览器将导航到页面并渲染一个新页面。在新选项卡中。
但是当 target 值是页面中某个位置的 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
类相当于放置一个 target
属性,该属性的值是一个 CSS 选择器,该选择器匹配当前元素(此处为 FORM 元素)。在这种情况下,提交表单将使用服务器的响应替换 FORM 元素的内容。
在服务器端,处理页面部分请求(page part request)只需要您将视图作为 PartialView
返回,或者将 _Layout
属性设置为 null
,这样就不会返回布局模板。
有关 Sircl 的部分页面加载的更多信息,请访问:
https://www.getsircl.com/Doc/v2/PartialLoading
使用页面部分请求重新渲染
所以,让我们更新我们的地址输入表单以使用页面部分请求(page part requests)。使用 Sircl 有几种方法可以做到这一点,让我们来看两个选项:
选项 1 – 仅更新省/州列表
在此解决方案中,我们将仅更新省/州列表,并且仅在更改国家时更新。
所以我们需要一个动作方法(我在这里使用 MVC),我们可以调用它来获取省/州列表(我的意思是获取整个 select 控件,而不仅仅是列表的内容,因为 select 控件是否存在也取决于所选的国家)。
[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
返回!在网络上传输时,我们只想返回 select 控件,而不是带有标题、导航栏和页脚的整个页面。
要创建“StateList.cshtml”视图,请将 StateCode
字段(包括 Razor IF
语句)移到一个单独的视图中。
@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
视图的调用。毕竟,我们可能正在编辑一个已经填写好的地址,并且已经设置了省/州。在这里,我们将对部分视图的调用包含在一个 DIV
元素中,该元素的 id
为 StateListContainer
。这是因为我们需要引用它。
<div id="StateListContainer">
<partial name="StateList" />
</div>
最后,我们需要定义当国家发生变化时,我们希望更新省/州列表。我们通过在国家控件上使用 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 调用将整个表单提交到 Index 方法。
对 Index 方法的“正常”调用(例如在导航到地址输入表单时)应该返回整个页面(表单以及页脚、页眉、导航栏等)。然而,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 元素会被替换。
sub-target
属性可以引用带有多个匹配项的类,以便可以在不更新整个表单的情况下更新多个元素。
通过将 StateCode 控件(包括其 Razor IF
语句)包装在一个可识别的元素中,并将 sub-target
属性指向 CountryCode
控件(触发元素)上的该元素,我们可以保持服务器端代码简单,同时仅替换 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);
}
(正如您所见,控制器代码并不感知 sub-target 的使用。)
最终我们做得与选项 1 相同:仅更新省/州列表。但这次,我们不需要为此创建第二个控制器操作方法,也不需要拆分我们的 Razor 视图。对我们原始代码的影响很小。
第三个解决方案的完整代码可在此处下载。
结论
同样,我们已经看到 Sircl 如何让我们编写动态应用程序——这次是使用服务器端渲染——通过添加声明式 HTML 属性和类,而不是编写命令式 JavaScript 代码,对代码的影响最小。
尽管在这篇文章中,我们举了一个简单的例子,但在现实世界中,许多复杂的表单都可以这样实现。下次,我们将进一步探讨这个问题,看看如何使用表单和 Sircl 来管理项目列表。
同时,在以下地址了解 Sircl 的全部信息:
https://www.getsircl.com/