Exit Console App at any Time During Any Input (C#)

前端 未结 2 1245
独厮守ぢ
独厮守ぢ 2021-01-17 05:23

I have a relatively large console app with multiple menu\'s and user inputs. I need to create a way for a user to \"Quit\" or \"Back\" at any time essentially break;

2条回答
  •  离开以前
    2021-01-17 05:45

    I haven't done a complex console application since the early 90s. If I have an app with "with multiple menu's and user inputs", I usually use something (Windows Forms, WPF, a web app) that supports that natively. But...

    If this is a big/complex enough project, and particularly if you have plans to write more than one of these, it might be worth writing a little framework based on the Model-View-Controller (MVC) pattern.

    In this case, we'll actually have two models, one that's reasonably complex that describes the program flow, and a second one that is a simple Dictionary that contains the user's answers. The Controller is a simple processing loop that executes the directives in the first model. The View is very simple, but we'll see that by segregating it out, there are some advantages.

    To do this, you will need to completely change the structure of your programming. I'm assuming it mostly looks like this:

     Console.WriteLine("UserInput #1");
     var response = Console.ReadLine();
     DoSomethingWith(response);
     Console.WriteLine("UserInput #2");
     response = Console.ReadLine();
     DoSomethingWith(response);
     // lather, rinse, repeat
    

    Instead, the flow of the program will be determined by that first model. So...

    The Models

    The first model is the important part, but let's get the second model out of the way first. The second model (the AnswerModel) is just a List, where an Answer looks something like:

    public class Answer {
        public string StepName { get; set; }
        public string VerbatimResponse { get; set; }
        public object TypedResponse { get; set; }
        public Type ResponseType { get; set; }
    }
    

    It represents the answer to a particular question. The List of answers represents the answers to all of the user's questions so far. It may be possible to play some generics games (likely with inheritance) to make the TypedResponse property actually be properly typed, this should be enough to get us started.

    The first model, the InputModel is the guts of the program. It will consist of a collection of ModelStep objects. The collection could just be a simple list (question 1, question 2, etc.) or it could be a complex graph. In particular, the EvalNextStep delegate property in the model shown below allows you to build a simple state machine (for example, if one of your questions is "What is your gender?", they you could have a separate path through the graph for males and for females).

    The InputModel would look something like this (you could adapt this for your needs):

    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 , string> AfterInputAction { get; set; }
        public Func, string, string> EvalNextStep { get; set; }
    }
    

    The StepName property is the key to everything (note that it corresponds to the StepName property of the AnswerModel). The prompt is the prompt you will use when prompting for an answer. I'm not sure if a UserInputType property is needed, but I envision it to look something like:

    public enum UserInputType {
        String,
        Integer,
        Numeric,
        Enum,
    }
    

    The two Validators are used to validate the user input. The TypeValidator class would likely be abstract with concrete subclasses like:

    • StringValidator
    • IntegerValidator
    • DoubleValidator
    • EnumValidator where T : enum

    A TypeValidator's role in live is to take the user's input, validate that it's the proper type, and then return either an error message or the response as a properly typed object.

    SpecificValidator objects would do additional validation. SpecificValidator is also likely an abstract class, with concrete subclasses like:

    • LessThanValidator where T : IComparable
    • GreaterThanValidator where T : IComparable
    • RangneValidator where T : IComparable

    The AdditionalValidator property is optional. It would provide additional validation if it was needed. It would return an error message if the validation failed.

    The AfterInputAction delegate would optionally point to a function that takes all of the answers so far and the current step name, and do something with that information if needed.

    The EvalNextStep delegate would take the same inputs as AfterInputAction delegate and return the "next step" to run. As noted above, this would allow you to create a simple "state machine". You may not need this, but it could make it interesting.

    The Controller

    The controller is the meat of the program, but it's real simple. At the start of your program, you'd hand the controller the InputModel and something that indicates the first step, and the controller would simply walk through the InputModel collection, prompting users and soliciting responses.

    However, since all user interaction is in one place, it would be easy to implement your "Quit" feature. You could also implement other similar features, like:

    • Back (Go back to the previous question and see your answer. You could build a "back stack" and allow users to go back more than once if you wanted)
    • Forward (if someone used "Back", allow them to go forward - likely with a "forward stack")
    • Skip (if the question is optional, a user could skip it)

    Again, since all the interaction is in the same code, you could easily provide some sort of consistent indication as to which of these commands (Quit, Back, etc.) was allowed.

    The View

    The temptation is to have the controller directly interact with the console using Console.WriteLine, Console.ReadLine, etc.

    However, there is some advantage to abstracting this into a View and defining the View using an interface. Something like:

    public interface IConsoleView {
        void Write(string stringToWrite);
        void WriteLine(string stringToWrite);
        string ReadLine(string prompt);
    }
    

    The default implementation of this interface would be braindead easy to create using the Console class. However, by making it an interface and using Dependency Injection to inject an interface implementation, you get several advantages:

    • You make your app testable - you could inject in a test View that played prompts and recorded answers, allowing you to test your logic
    • You could have a ResponseFileView that allow you to accept a "response file" that consists of the answers to the questions, allowing automation of your UI
    • etc.

    You might want to extend the definition of the interface from what I have above (possibly having do-nothing implementations of the extra functions in your default Console class implementation). For example:

    void WriteStepName(string stepName);
    void WriteUserResponse (string userResponse);
    

    Functions like these might be useful in the test and response file scenarios. You would provide empty implementations in the normal view.

    Sorry this went on a for a while, but I've been thinking about it the last day or so. Whatever you do, don't try to do this with additional threads, that will just cause you headaches.

提交回复
热议问题