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

如何为非 JS 用户实现渐进式披露 UI

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2023年2月7日

CPOL

3分钟阅读

viewsIcon

4753

在禁用 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 日:初始版本
© . All rights reserved.