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;>
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 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 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:
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 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 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.