如何为非 JS 用户实现渐进式披露 UI
在禁用 JS 的浏览器上实现渐进式披露 UI。
什么是渐进式披露 UI?
渐进式披露 UI 是一种用户界面,其内容和操作会根据用户的响应而变化。例如,请参见以下场景。
根据用户对问题“您有医生预约吗?”的回答,UI 会动态变化。如果用户回答是,则下一个问题是“这是您第一次来吗?”。否则,将是“您想预约吗?”。请参见下面的流程图。
根据对第二个问题的回答,下一个问题/内容和操作会发生变化。
如何实现这一点?
在启用 JavaScript 的浏览器中,这很容易。您可以使用单选按钮表示“是/否”,并将其设置为自动提交为真,或者更好的方法是使用 Ajax 调用并仅加载包含适当内容和操作的部分页面。
对于禁用 JavaScript 的浏览器(非 JS 用户)来说,情况并非如此。
非 JS / 禁用 JavaScript 的浏览器的问题
在非 JS 中,自动提交和 Ajax 调用将不起作用。因此,上面讨论的任何解决方案都将无法用于非 JS 用户。如果您使用单选按钮,当用户单击“是/否”时,页面更新不会发生,这将导致错误的 UI。该应用程序不会像在渐进式披露中那样具有响应性和准确性。例如,请参见以下屏幕截图。
当您选择是并单击提交时,将显示以下页面。
现在为第一个问题选择否。暂时不要单击提交,并注意 UI。这将导致错误的 UI。请参见上面的流程图,该流程图显示,如果没有医生预约,则需要询问用户是否要安排预约。
第二个问题应该是“您想预约吗?”。比较下面错误和正确的 UI。
在这种错误的 UI 中,内容、问题和操作不正确,这可能会导致单击“提交”时在数据库中输入错误的内容。请参见以下示例:
请注意,所有这些问题都发生在非 JS 浏览器中(禁用 JavaScript 的浏览器)。要测试这一点,您可以禁用浏览器中的 JavaScript,或者使用 Chrome 中的“切换 JavaScript”扩展程序。
那么,您如何解决此问题?
这是解决方案。
在 Visual Studio 2022 中,创建一个新的 ASP.NET Core Web 应用程序项目 — NonJSProgressiveDisclosureUI
这是最终的 解决方案资源管理器
添加Enums文件夹并创建新的Enum YesNoType
。
namespace NonJSProgressiveDisclosureUI.Enums
{
public enum YesNoType
{
Unknown,
Yes,
No
}
}
在Models文件夹中添加新模型IndexViewModel
using NonJSProgressiveDisclosureUI.Enums;
namespace NonJSProgressiveDisclosureUI.Models
{
public class IndexViewModel
{
public YesNoType AnswerToHaveTheDoctorAppointmentQuestionIs { get; set; }
public YesNoType AnswerToIsThisYourFirstVisitQuestionIs { get; set; }
public YesNoType AnswerToScheduleAnAppointmentQuestionIs { get; set; }
public bool ShowRadioButtons { get; set; }
}
}
更新Program.cs — 添加缓存服务
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddMemoryCache();
更新 Home Controller
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using NonJSProgressiveDisclosureUI.Enums;
using NonJSProgressiveDisclosureUI.Models;
using System.Diagnostics;
namespace NonJSProgressiveDisclosureUI.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IMemoryCache _memoryCache;
public HomeController(ILogger<HomeController> logger, IMemoryCache memoryCache)
{
_logger = logger;
_memoryCache = memoryCache;
}
[HttpGet]
public IActionResult Index()
{
var viewModel = new IndexViewModel();
viewModel.ShowRadioButtons = false;
return View(viewModel);
}
[HttpPost]
public IActionResult Index(IndexViewModel viewModel, string submit)
{
var updatedViewModel = viewModel;
if (!viewModel.ShowRadioButtons)
{
updatedViewModel = SetIndexViewModel(viewModel, submit);
}
return View(updatedViewModel);
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0,
Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel
{ RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
private IndexViewModel SetIndexViewModel
(IndexViewModel viewModel, string submit)
{
var cacheKey = "doctorAppointmentKey";
IndexViewModel cachedViewModel =
(IndexViewModel)_memoryCache.Get(cacheKey) ?? viewModel;
switch (submit)
{
case "AnswerToHaveTheDoctorAppointmentQuestionIsYes":
{
cachedViewModel.AnswerToHaveTheDoctorAppointmentQuestionIs =
YesNoType.Yes;
break;
}
case "AnswerToHaveTheDoctorAppointmentQuestionIsNo":
{
cachedViewModel.AnswerToHaveTheDoctorAppointmentQuestionIs =
YesNoType.No;
break;
}
case "AnswerToIsThisYourFirstVisitQuestionIsYes":
{
cachedViewModel.AnswerToIsThisYourFirstVisitQuestionIs =
YesNoType.Yes;
break;
}
case "AnswerToIsThisYourFirstVisitQuestionIsNo":
{
cachedViewModel.AnswerToIsThisYourFirstVisitQuestionIs =
YesNoType.No;
break;
}
case "AnswerToScheduleAnAppointmentQuestionIsYes":
{
cachedViewModel.AnswerToScheduleAnAppointmentQuestionIs =
YesNoType.Yes;
break;
}
case "AnswerToScheduleAnAppointmentQuestionIsNo":
{
cachedViewModel.AnswerToScheduleAnAppointmentQuestionIs =
YesNoType.No;
break;
}
case "Submit":
break;
}
//Cache
var cacheExpiryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpiration = DateTime.Now.AddMinutes(20)
};
_memoryCache.Set(cacheKey, cachedViewModel, cacheExpiryOptions);
return cachedViewModel;
}
}
}
更新Index.cshtml
@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome to Our Hospitals</h1>
@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
@Html.AntiForgeryToken()
<partial name="../_DoctorAppointment" model="Model" />
if (Model.ShowRadioButtons)
{
<div style="display:flex; margin-top:35px">
<button type="submit" id="submit" name="submit" value="submit">
Submit
</button>
</div>
}
}
</div>
在 Views > Shared 文件夹中添加以下视图。
__DoctorAppointment.cshtml
@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel
<partial name="../_DoctorAppointmentQuestion" model="Model"/>
@if(Model.AnswerToHaveTheDoctorAppointmentQuestionIs == YesNoType.Yes)
{
<partial name="../_IsThisYourFirstVisitQuestion" model="Model" />
@if (Model.AnswerToIsThisYourFirstVisitQuestionIs == YesNoType.Yes)
{
<partial name="../_SubmitIdProof" model="Model" />
}
else if (Model.AnswerToIsThisYourFirstVisitQuestionIs == YesNoType.No)
{
<partial name="../_ProvideRegistrationNumber" model="Model" />
}
}
else if (Model.AnswerToHaveTheDoctorAppointmentQuestionIs == YesNoType.No)
{
<partial name="../_ScheduleAnAppointmentQuestion" model="Model" />
}
__DoctorAppointmentQuestion.cshtml
@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel
<input asp-for="ShowRadioButtons" type="hidden" />
@{
var buttonStyleSelected = "text-align:center; width:30%;
font-weight:600; color:darkslateblue; background-color:yellow";
var buttonStyleDefault = "text-align:center; width:30%;
font-weight:600; color:darkslateblue";
var buttonStyleYes = buttonStyleDefault;
var buttonStyleNo = buttonStyleDefault;
}
<div style="text-align:left;margin-top:25px">
Do you have the doctor appointment
</div>
@if (Model.AnswerToHaveTheDoctorAppointmentQuestionIs == YesNoType.Yes)
{
buttonStyleYes = buttonStyleSelected;
}
else if (Model.AnswerToHaveTheDoctorAppointmentQuestionIs == YesNoType.No)
{
buttonStyleNo = buttonStyleSelected;
}
@if (!Model.ShowRadioButtons)
{
<div style="display:flex">
<button style="@buttonStyleYes" type="submit"
id="AnswerToHaveTheDoctorAppointmentQuestionIsYes"
name="submit" value="AnswerToHaveTheDoctorAppointmentQuestionIsYes">Yes</button>
<button style="@buttonStyleNo" type="submit"
id="AnswerToHaveTheDoctorAppointmentQuestionIsNo"
name="submit" value="AnswerToHaveTheDoctorAppointmentQuestionIsNo">No</button>
</div>
}
else
{
<div style="text-align:left;margin-top:15px">
@Html.RadioButtonFor(m => m.AnswerToHaveTheDoctorAppointmentQuestionIs,
YesNoType.Yes, new { id="AnswerToHaveTheDoctorAppointmentQuestionIsYes" } )
<label for="yes" style="width:8%">Yes</label>
@Html.RadioButtonFor(m => m.AnswerToHaveTheDoctorAppointmentQuestionIs,
YesNoType.No, new { id="AnswerToHaveTheDoctorAppointmentQuestionIsNo" } )
<label for="yes" style="width:8%">No</label>
</div>
}
_IsThisYourFirstVisitQuestion.cshtml
@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel
@{
var buttonStyleSelected = "text-align:center; width:30%;
font-weight:600; color:darkslateblue; background-color:yellow";
var buttonStyleDefault = "text-align:center; width:30%;
font-weight:600; color:darkslateblue";
var buttonStyleYes = buttonStyleDefault;
var buttonStyleNo = buttonStyleDefault;
}
<div style="text-align:left; margin-top:25px">
Is this your first visit
</div>
@if (Model.AnswerToIsThisYourFirstVisitQuestionIs == YesNoType.Yes)
{
buttonStyleYes = buttonStyleSelected;
}
else if (Model.AnswerToIsThisYourFirstVisitQuestionIs == YesNoType.No)
{
buttonStyleNo = buttonStyleSelected;
}
@if (!Model.ShowRadioButtons)
{
<div style="display:flex">
<button style="@buttonStyleYes" type="submit"
id="AnswerToIsThisYourFirstVisitQuestionIsYes" name="submit"
value="AnswerToIsThisYourFirstVisitQuestionIsYes">Yes</button>
<button style="@buttonStyleNo" type="submit"
id="AnswerToIsThisYourFirstVisitQuestionIsIsNo" name="submit"
value="AnswerToIsThisYourFirstVisitQuestionIsNo">No</button>
</div>
}
else
{
<div style="text-align:left;margin-top:15px">
@Html.RadioButtonFor(m => m.AnswerToIsThisYourFirstVisitQuestionIs,
YesNoType.Yes, new { id="AnswerToIsThisYourFirstVisitQuestionIsYes" } )
<label for="yes" style="width:8%">Yes</label>
@Html.RadioButtonFor(m => m.AnswerToIsThisYourFirstVisitQuestionIs,
YesNoType.No, new { id="AnswerToIsThisYourFirstVisitQuestionIsNo" } )
<label for="yes" style="width:8%">No</label>
</div>
}
_ProvideRegistrationNumber.cshtml
<div style="display:flex;margin-top:30px;">
<label for="regNo" style="width:30%;text-align:left">
Please provide registration number: </label>
<input type="text" style="width:30%;" id="regNo" name="regNo">
</div>
_ScheduleAnAppointmentQuestion.cshtml
@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel
@{
var buttonStyleSelected = "text-align:center; width:30%;
font-weight:600; color:darkslateblue; background-color:yellow";
var buttonStyleDefault = "text-align:center; width:30%;
font-weight:600; color:darkslateblue";
var buttonStyleYes = buttonStyleDefault;
var buttonStyleNo = buttonStyleDefault;
}
<div style="text-align:left; margin-top:25px">
Do you want to schedule an appointment
</div>
@if (Model.AnswerToScheduleAnAppointmentQuestionIs == YesNoType.Yes)
{
buttonStyleYes = buttonStyleSelected;
}
else if (Model.AnswerToScheduleAnAppointmentQuestionIs == YesNoType.No)
{
buttonStyleNo = buttonStyleSelected;
}
@if (!Model.ShowRadioButtons)
{
<div style="display:flex">
<button style="@buttonStyleYes" type="submit"
id="AnswerToScheduleAnAppointmentQuestionIsYes" name="submit"
value="AnswerToScheduleAnAppointmentQuestionIsYes">Yes</button>
<button style="@buttonStyleNo" type="submit"
id="AnswerToScheduleAnAppointmentQuestionIsNo" name="submit"
value="AnswerToScheduleAnAppointmentQuestionIsNo">No</button>
</div>
}
else
{
<div style="text-align:left;margin-top:15px">
@Html.RadioButtonFor(m => m.AnswerToScheduleAnAppointmentQuestionIs,
YesNoType.Yes, new { id="AnswerToScheduleAnAppointmentQuestionIsYes" } )
<label for="yes" style="width:8%">Yes</label>
@Html.RadioButtonFor(m => m.AnswerToScheduleAnAppointmentQuestionIs,
YesNoType.No, new { id="AnswerToScheduleAnAppointmentQuestionIsNo" } )
<label for="yes" style="width:8%">No</label>
</div>
}
_SubmitIdProof.cshtml
<div style="text-align:left; margin-top:30px">
<label for="idProof" style="width:25%;text-align:left">
Please Submit ID Proof for Registration: </label>
<button>Browse</button>
</div>
理解代码
- Home controller >
SetIndexViewModel
方法 — 根据用户的选择,模型值将被更新并缓存。 - 创建所有相关的分部页。
- _DoctorAppointment.cshtml — 这是加载包含正确问题、内容和应用程序的适当分部页的逻辑所在。
- 使用按钮代替单选按钮,并且模型值在 Home Controller 中的
SetIndexViewModel
方法中更新。
解决方案
现在运行该应用程序,看看新解决方案如何解决所有问题。您可以将其与上面的流程图进行比较。
当用户选择是时,它会检查是否是第一次来。
当用户为第二个问题选择是时,它会要求提供身份证明,以将其注册为新用户。
当用户为第二个问题选择否时,它会要求提供注册号码。
现在,当用户为第一个问题选择否时,第二个问题会相应更改。
您可能已经注意到第二个问题已适当更改。
假设用户再次为第一个问题选择是。UI 还应保留他之前对第二个问题的选择,即在本例中为“否”。此外,还需要适当更改第二个问题。
因此,它在所有情况下都显示了正确的 UI。
如果要测试单选按钮,请在 Home Controller 中设置ShowRadioButtons=true
并运行该应用程序。
希望您觉得这很有趣和有用。
历史
- 2023 年 2 月 7 日:初始版本