在ASP.NET应用程序中创建具有复杂规则的向导式导航系统的有效且可维护的方法是什么
本文关键字:有效 系统 导航 是什么 方法 维护 向导 应用程序 NET 创建 规则 | 更新日期: 2023-09-27 18:25:48
我正在团队中工作,用C#更新一个商业web应用程序。我们正在使用Telerik的RadControls。该应用程序以类似向导的方式引导用户完成一组任务,但也允许用户跳回以前的步骤并进行修改。此外,有些步骤具有复杂的验证方法。例如,完成步骤5中的任务后,步骤7、8和9将可用。此外,如果您在步骤2更改任何设置,则必须重新执行该点之后的所有操作,因此必须禁用步骤3之后的所有步骤。
- 屏幕左侧有一个导航栏(TelerikRadPanel),按顺序列出所有步骤。取决于关于您在流程中所处的位置用户,有些被禁用
- 页面顶部有一个下拉框(TelerikRadToolBar上的SplitButton),其中还包含所有步骤
- 有向前和向后按钮,可以移动到下一个或过程中的前一步骤
每个"步骤"都有自己的页面,因此单击"步骤1"的链接将转到步骤1.aspx
导航代码散布在主页面上。有一些方法可以处理步骤启用逻辑,这些方法基本上是导航面板所选索引上的大量切换语句。我想重写所有这些功能,并将其放入一个"NavigationManager"类中,该类引用了所有必要的控件(尽管我对此持开放态度)。我正在寻找一个解决方案,将:
- 了解用户在流程中的位置,以及允许用户访问的位置,以便在每次加载页面时启用和禁用导航项目。每个步骤都应该有一种验证过程,使其能够决定将用户发送到哪里
- 了解如何为每个控件启用和禁用导航项目。如何将其连接起来?每个导航控件的按钮索引都不可预测地不同
- 了解这些步骤是如何关联的,以便用户可以单击"下一步"按钮并进入下一个顺序步骤
- 可维护性-当前的系统非常复杂,修复一个错误会导致其他错误
- 高效-处理每次页面加载不需要花费太多时间
我想我真正遇到的困难是如何在的某个地方定义所有这些步骤一次,以便系统能够利用它们。我曾想过在enum
中定义所有步骤,但我预见到了许多用于启用导航控件上的步骤按钮的switch语句。
我在谷歌上搜索了我能想到的所有相关关键词,但找不到任何有用的东西。我知道这不是一个独特的问题。我看到很多类似导航系统的网络应用程序的例子,比如TurboTax。
所以,我的问题是:
- 我如何在一个地方定义我的所有步骤,以便整个程序知道如何处理它们
- 如何使用这些定义来确定用户可以访问哪些步骤,然后启用适当的导航项
看起来您需要了解系统中的一些内容。
- 首先,您需要每个步骤的步骤和任务列表
- 其次,您需要了解每个步骤中每个任务的状态。这可以很简单,比如一个位标志,也可以是一个具有更复杂状态的枚举,比如Not Started、Incomplete和Complete。每个任务都应该知道自己的状态以及确定该状态的标准。一旦一个步骤中的所有任务都完成,该步骤就会自动被视为完成。您可以选择将页面URL与每个步骤相关联,甚至将控件ID与每个任务相关联
- 最后,您需要一个全局上下文,它可以在页面加载时了解所有任务的状态,并保留您提到的复杂规则。一种方法是定义必须完成的任务和/或步骤的列表,并用于基于此list.All(x x=>true)设置属性CanBeVisible;这可以在数据库、XML中完成,只要映射加载到上下文中,并为每个任务更新状态信息即可。然后,任何导航控件都可以进入这个全局上下文,并确切地知道要显示哪些选项
其想法是在一个中心位置封装可见性依赖项到每个任务状态的映射,然后任何其他UI元素(如导航面板)都可以使用此信息来仅呈现那些满足任何给定步骤和任务集可见性标准的控件。
CKirb250回复:很抱歉,我不得不把我的评论放在这里,因为评论框中的格式很糟糕。
-
是的,我确实需要一份步骤清单。我应该把它们放在哪里?它们应该是一个简单的
enum
吗?它们是否应该以XML格式sommwhere进行布局,以便我可以用键值引用它们,应该向用户显示它们的名称,以及该步骤对应的aspx页面? -
这是在数据库中跟踪的。为了了解每个步骤的状态,将查询数据库。
-
如何定义复杂的规则?我在想象一个巨大的switch语句,上面写着
if (currentStep == steps.Step1) { if (page.IsFilledOut) { enableSteps(1, 2, 3, 4, 5); } }
。
这里有一种在XML中设置这一点的方法-每个元素都应该定义为一个类,如果在数据库中跟踪(如建议的那样),那么您就有了一些表(至少是Step、Task、VisibilityDependency。)您可以从DB生成XML(或者直接从DB填充类)。我使用XML作为一个简单的例子来可视化我的想法:
<WizardSchema>
<Steps>
<Step>
<StepID>1</StepID>
<StepOrder>1</StepOrder>
<StepTitle>First Step</StepTitle>
<StepUrl>~/step1.aspx</StepUrl>
<Tasks>
<Task>
<TaskID>1</TaskID>
<TaskOrder>1</TaskOrder>
<TaskPrompt>Enter your first name:</TaskPrompt>
<TaskControlID>FirstNameTextBox</TaskControlID>
<VisibilityDependencyList></VisibilityDependencyList>
<IsCompleted>True</IsCompleted>
</Task>
<Task>
<TaskID>2</TaskID>
<TaskOrder>2</TaskOrder>
<TaskPrompt>Enter your last name:</TaskPrompt>
<TaskControlID>LastNameTextBox</TaskControlID>
<VisibilityDependencyList>
<VisibilityDependency StepID="1" TaskID="1" />
</VisibilityDependencyList>
<IsCompleted>False</IsCompleted>
</Task>
</Tasks>
</Step>
<Step>
<StepID>2</StepID>
<StepOrder>2</StepOrder>
<StepTitle>Second Step</StepTitle>
<StepUrl>~/step2.aspx</StepUrl>
<Tasks>
<Task>
<TaskID>3</TaskID>
<TaskOrder>1</TaskOrder>
<TaskPrompt>Enter your phone number type:</TaskPrompt>
<TaskControlID>PhoneNumberTypeDropDown</TaskControlID>
<VisibilityDependencyList>
<VisibilityDependency StepID="1" />
<!-- Not setting a TaskID attribute here means ALL tasks should be complete in Step 1 for this dependency to return true -->
</VisibilityDependencyList>
<IsCompleted>False</IsCompleted>
</Task>
<Task>
<TaskID>4</TaskID>
<TaskOrder>2</TaskOrder>
<TaskPrompt>Enter your phone number:</TaskPrompt>
<TaskControlID>PhoneNumberTextBox</TaskControlID>
<VisibilityDependencyList>
<VisibilityDependency StepID="1" />
<VisibilityDependency StepID="2" TaskID="1" />
</VisibilityDependencyList>
<IsCompleted>False</IsCompleted>
</Task>
</Tasks>
</Step>
</Steps>
</WizardSchema>
现在我想的是,你的导航控件会轮询你的全局上下文,现在为此编写一些代码,只是为了让你知道我将如何做到这一点。
这里有一些代码来表示这个模式,还有一个方法来返回可以在页面上显示的所有任务;没有switch语句!
using System;
using System.Linq;
using System.Collections.Generic;
namespace Stackoverflow.Answers.WizardSchema
{
// Classes to represent your schema
public class VisibilityDependency
{
public int StepID { get; set; }
public int? TaskID { get; set; } // nullable to denote lack of presense
}
public class Task
{
public int TaskID { get; set; }
public int TaskOrder { get; set; }
public string TaskControlID { get; set; }
public bool IsComplete { get; set; }
public List<VisibilityDependency> VisibilityDependencyList { get; set; }
}
public class Step
{
// properties in XML
public int StepID { get; set; }
public string StepTitle { get; set; }
public List<Task> Tasks { get; set; }
}
// Class to act as a global context
public class WizardSchemaProvider
{
/// <summary>
/// Global variable to keep state of all steps (which contani all tasks)
/// </summary>
public List<Step> Steps { get; set; }
/// <summary>
/// Current step, determined by URL or some other means
/// </summary>
public Step CurrentStep { get { return null; /* add some logic to determine current step from URL */ } }
/// <summary>
/// Default Constructor; can get data here to populate Steps property
/// </summary>
public WizardSchemaProvider()
{
// Init; get your data from DB
}
/// <summary>
/// Utility method - returns all tasks that match visibility dependency for the current page;
/// Designed to be called from a navigation control;
/// </summary>
/// <param name="step"></param>
/// <returns></returns>
private IEnumerable<Task> GetAllTasksToDisplay(Step step)
{
// Let's break down the visibility dependency for each one by encapsulating into a delegate;
Func<VisibilityDependency, bool> isVisibilityDependencyMet = v =>
{
// Get the step in the visibility dependency
var stepToCheck = Steps.First(s => s.StepID == v.StepID);
if (null == v.TaskID)
{
// If the task is null, we want all tasks for the step to be completed
return stepToCheck
.Tasks // Look at the List<Task> for the step in question
.All(t => t.IsComplete); // make sure all steps are complete
// if the above is all true, then the current task being checked can be displayed
}
// If the task ID is not null, we only want the specific task (not the whole step)
return stepToCheck
.Tasks
.First(t => t.TaskID == v.TaskID) // get the task to check
.IsComplete;
};
// This Func just runs throgh the list of dependencies for each task to return whether they are met or not; all must be met
var tasksThatCanBeVisible = step
.Tasks
.Where(t => t.VisibilityDependencyList
.All(v => isVisibilityDependencyMet(v)
));
return tasksThatCanBeVisible;
}
public List<string> GetControlIDListForTasksToDisplay(Step step)
{
return this.GetAllTasksToDisplay(this.CurrentStep).Select(t => t.TaskControlID).ToList();
}
}
}
让我知道这是否足以让你自己的想法以一种干净的方式重构你的代码。我已经开发并使用了许多向导风格的系统,我亲眼看到了你所描述的;也就是说,如果从一开始就没有很好的架构,那么它很快就会变得一团糟。祝你好运
您看过asp:Wizard
控件吗?这里有一些用法示例:ASP.NET 2.0向导控件和文档msdn.microsoft
策略可以是为每个步骤创建一个自定义控件,然后将它们添加到向导中。通过这种方式,每个步骤的代码都可以被隔离,并且包含向导的单个页面可以使用这些控件的接口来处理每个步骤的更改!
我最近设计了自己的"向导"控件;当然这是针对WinForms的,但我认为我的方法有一些优点,因为它不同于其他一些看起来很常见的向导。
我将这个过程从"基于步骤"的方法转变为"基于任务"的方法。也就是说,没有一个步骤知道任何其他步骤或它们之间的关系,任务知道所有步骤。当一个步骤"完成"时,控制返回给任务,然后任务收集任何OUTPUT状态,并将其作为INPUT状态提供给下一个步骤。因为任务知道步骤的每个单独类型(通过静态类型),所以它可以以类型安全的方式执行任何适当的操作。(任务和任务UI应该是独立的,但有相关的组件:任务完成工作,并与UI一起处理用户导航。)
然后使用一致的辅助方法/类,在只需要最少的连接来完成"自定义工作"的琐碎情况下,沿着步骤前进。(我更喜欢将逻辑"保存在代码中",因为它经常可以快速逃离标记语言/"规则对象"的界限。)
现在,它真正不同的地方是如何处理UI;-)我建议保持UI显式(在ASCX或模板中),不要尝试动态生成它。。。除非真的有充分的理由。
希望这能给你的设计带来一些启示!