自 90 年代初以来,我还没有做过复杂的控制台应用程序。如果我有一个“具有多个菜单和用户输入”的应用程序,我通常会使用本机支持的东西(Windows 窗体、WPF、Web 应用程序)。但是……
如果这是一个足够大/足够复杂的项目,特别是如果您计划编写多个项目,那么可能值得编写一个基于模型-视图-控制器 (MVC) 模式的小框架。
在这种情况下,我们实际上有两个模型,一个是相当复杂的描述程序流程的模型,另一个是包含用户答案的简单 Dictionary。控制器是一个简单的处理循环,执行第一个模型中的指令。 View 非常简单,但我们会看到,通过将其分离出来,会有一些优势。
为此,您需要彻底改变程序的结构。我假设它大多看起来像这样:
Console.WriteLine("UserInput #1");
var response = Console.ReadLine();
DoSomethingWith(response);
Console.WriteLine("UserInput #2");
response = Console.ReadLine();
DoSomethingWith(response);
// lather, rinse, repeat
相反,程序的流程将由第一个模型决定。所以...
模型
第一个模型是重要的部分,但让我们先把第二个模型排除在外。第二个模型(AnswerModel)只是一个List<Answer>,其中Answer 看起来像:
public class Answer {
public string StepName { get; set; }
public string VerbatimResponse { get; set; }
public object TypedResponse { get; set; }
public Type ResponseType { get; set; }
}
它代表特定问题的答案。答案列表表示到目前为止用户所有问题的答案。可以玩一些泛型游戏(可能带有继承)以使TypedResponse 属性实际上被正确键入,这应该足以让我们开始。
第一个模型,InputModel 是程序的核心。它将由ModelStep 对象的集合组成。该集合可能只是一个简单的列表(问题 1、问题 2 等),也可能是一个复杂的图表。特别是,下面显示的模型中的EvalNextStepdelegate 属性允许您构建一个简单的状态机(例如,如果您的问题之一是“您的性别是什么?”,那么您可以有一条单独的路径通过图表男性和女性)。
InputModel 看起来像这样(您可以根据自己的需要进行调整):
public class ModelStep {
public string StepName { get; set; }
public string Prompt { get; set; }
public bool IsOptional {get; set;}
public UserInputType InputType { get; set; }
public TypeValidator BasicValidator { get; set; }
public SpecificValidator AdditionalValidator { get; set; }
public Action <List<Answer>, string> AfterInputAction { get; set; }
public Func<List<Answer>, string, string> EvalNextStep { get; set; }
}
StepName 属性是一切的关键(注意它对应于 AnswerModel 的 StepName 属性)。提示是您在提示答案时将使用的提示。我不确定是否需要 UserInputType 属性,但我设想它看起来像:
public enum UserInputType {
String,
Integer,
Numeric,
Enum,
}
这两个验证器用于验证用户输入。 TypeValidator 类可能是 abstract 并带有具体的子类,例如:
StringValidator
IntegerValidator
DoubleValidator
EnumValidator<T> where T : enum
TypeValidator 在 live 中的作用是接受用户的输入,验证它是否是正确的类型,然后返回错误消息或作为正确类型的对象的响应。
SpecificValidator 对象会做额外的验证。 SpecificValidator 也可能是 abstract 类,具有具体的子类,例如:
LessThanValidator<T> where T : IComparable
GreaterThanValidator<T> where T : IComparable
RangneValidator<T> where T : IComparable
AdditionalValidator 属性是可选的。如果需要,它将提供额外的验证。如果验证失败,它将返回错误消息。
AfterInputAction 委托可以选择指向一个函数,该函数获取到目前为止的所有答案和当前步骤名称,并在需要时使用该信息执行某些操作。
EvalNextStep 委托将采用与 AfterInputAction 委托相同的输入并返回“下一步”以运行。如上所述,这将允许您创建一个简单的“状态机”。你可能不需要这个,但它可以让它变得有趣。
控制器
控制器是程序的核心,但它非常简单。在程序开始时,您将 InputModel 和指示第一步的东西交给控制器,控制器将简单地遍历 InputModel 集合,提示用户并征求响应。
但是,由于所有用户交互都在一个地方,因此很容易实现您的“退出”功能。您还可以实现其他类似的功能,例如:
- 返回 (返回上一个问题并查看您的答案。您可以构建一个“返回堆栈”并允许用户多次返回)
- 前进(如果有人使用“后退”,允许他们前进 - 可能使用“前进堆栈”)
- 跳过(如果问题是可选的,用户可以跳过)
同样,由于所有交互都在同一个代码中,您可以轻松地提供某种一致的指示,说明允许使用哪些命令(退出、返回等)。
视图
诱惑是让控制器使用 Console.WriteLine、Console.ReadLine 等直接与控制台交互。
但是,将其抽象为 View 并使用 interface 定义 View 有一些优势。比如:
public interface IConsoleView {
void Write(string stringToWrite);
void WriteLine(string stringToWrite);
string ReadLine(string prompt);
}
这个接口的默认实现很容易使用 Console 类创建。但是,通过将其设为接口并使用 依赖注入 来注入接口实现,您将获得以下几个优势:
- 您可以让您的应用可测试 - 您可以注入一个播放提示和记录答案的测试视图,以便您测试您的逻辑
- 您可以有一个 ResponseFileView,允许您接受包含问题答案的“响应文件”,从而实现 UI 的自动化
- 等
您可能想从我上面的内容中扩展接口的定义(可能在默认的Console 类实现中对额外的函数进行无操作实现)。例如:
void WriteStepName(string stepName);
void WriteUserResponse (string userResponse);
像这样的函数在测试和响应文件场景中可能很有用。您将在普通视图中提供空实现。
抱歉,这件事持续了一段时间,但我在最后一天左右一直在考虑这件事。无论你做什么,都不要试图用额外的线程来做这件事,那只会让你头疼。