Skip to content

Instantly share code, notes, and snippets.

@akfish
Last active December 27, 2015 08:29
Show Gist options
  • Save akfish/7296821 to your computer and use it in GitHub Desktop.
Save akfish/7296821 to your computer and use it in GitHub Desktop.
Mock up data-driven interactive data input framework for C# console app

Mock up data-driven interactive data input framework for C# console app

The old way

Let's say that we have a class

public class Account
{
    public string HostName { get; set; }
    public int Port { get; set; }
    public string Email { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public bool UseSSL { get; set; }
    public AuthMethod AuthMethod { get; set; }
    public bool IsPrimary { get; set; }
}

And we want to make a console application to prompt user to inputting each fields

Console.Write("Input value for field:");

We want different behaviour for diffrent types: yes/no choice for bool, list for Enum.

Get value from user:

var value = Console.ReadLine();
// And we need validation
if (!IsValid(value))
{
    // TODO: Tell user what's wrong
    // TODO: Go back to Line 15 again
}
// TODO: And there's conversion

// And repeat all this for all fields
// Then we can finally get what we want
var account = new Account(/*all the sh*t from user*/);

Imaging doing this over and over and over from project to project. Just how painful it will be! :(

The New Way

Let's do it diffrently this time. We have a same class with a little extra work:

[FormValidator(typeof(AccountValidator))]
[FormHandler(typeof(AccountHandler))]
[FormSection(0, "Basic Information")]
[FormSection(1, "Connection Settings")]
public class Account
{
    [FormEntry(Section = 0, Step = 0)]
    public string Email { get; set; }
    [FormEntry(Section = 0, Step = 1)]
    public string Password { get; set; }
    
    [FormEntry(Section = 1, Step = 0)]
    public string UserName { get; set; }
    [FormEntry(Section = 1, Step = 1)]
    public string HostName { get; set; }
    [FormEntry(Section = 1, Step = 2)]
    public int Port { get; set; }
    [FormEntry(Section = 1, Step = 3)]
    public bool UseSSL { get; set; }
    [FormEntry(Section = 1, Step = 4)]
    public AuthMethod AuthMethod { get; set; }
    [FormEntry(Section = 1, Step = 5)]
    public bool IsPrimary { get; set; }
}

Also make a validator class.

// Validator class, plan and simple
public class AccountValidator
{
    // No more attributes. Here we use convention over code
    // bool IsValid{FieldName}(string fieldValue, ref reason_for_error)
    public bool IsValidEmail(string toValidate, ref string error) {}
    // And one for each field
}

Just to make it more realistic, say we want more control over the process.

We all know that the UserName and HostName for logging in can be inferred from email address. So it would be user-friendly to provide default value for these fields.

This can be done with a handler class. The base class FormHandler provides some infrastructure methods.

public class AccountHandler : FormHandler
{
    // Again, COC
    // This will be executed after a valid email value is aquired
    public void AfterEmail(string value)
    {
        // We all know that the UserName and HostName for logging in 
        // can be inferred from email address
        // So it would be user-friendly to provide default value for these fields
        var split = value.Split('@');
        // Use method from base class
        ProvideDefaultValue("UserName", split[0]);
        ProvideDefaultValue("HostName", "imap." + split[1])
        // With a little magic, these will show up when user begins to input these field
    }
}

With these little extra amout of work, we are only one line of code away from our goal:

var account = HyperConsole.ReadObject<Account>();

Then magically, the console will interactively ask user to input each fields, do validation with validator class and handle some special logic as defined by handler class.

Cool!

Execution Result From NewWay.cs

(1/2) Basic Information

**Email:**aaa

Invalid email address, try again!

**Email:**akfish@gmail.com

Password:(Not shown)

(2/2) Connection Settings

UserName:(akfish)(User pressed enter to accept default value)

HostName:(imap.gmail.com)(User pressed enter to accept default value)

(Port, not intresting)

**Use SSL [y/n]:**wtf?

Please input y(es) or n(0)

**Use SSL [y/n]:**y (yeah! It knew we were dealing with bool value. So prompt is different.)

Auth Method

**0. Auto **

**1. Login **

**2. OAuth2 **

**Your Choice:(0)**2 (Same trick for Enum)

(And that's how it's done!)

// Let's do it diffrently this time
// Just a same class with a little extra work
[FormValidator(typeof(AccountValidator))]
[FormHandler(typeof(AccountHandler))]
[FormSection(0, "Basic Information")]
[FormSection(1, "Connection Settings")]
public class Account
{
[FormEntry(Section = 0, Step = 0)]
public string Email { get; set; }
[FormEntry(Section = 0, Step = 1)]
public string Password { get; set; }
[FormEntry(Section = 1, Step = 0)]
public string UserName { get; set; }
[FormEntry(Section = 1, Step = 1)]
public string HostName { get; set; }
[FormEntry(Section = 1, Step = 2)]
public int Port { get; set; }
[FormEntry(Section = 1, Step = 3)]
public bool UseSSL { get; set; }
[FormEntry(Section = 1, Step = 4)]
public AuthMethod AuthMethod { get; set; }
[FormEntry(Section = 1, Step = 5)]
public bool IsPrimary { get; set; }
}
// Validator class, plan and simple
public class AccountValidator
{
// No more attributes. Here we use convention over code
// bool IsValid{FieldName}(string fieldValue, ref reason_for_error)
public bool IsValidEmail(string toValidate, ref string error) {}
// And one for each field
}
// Usually we want more control over the process
// FormHandler will provide some infrastructure
public class AccountHandler : FormHandler
{
// Again, COC
// This will be executed after a valid email value is aquired
public void AfterEmail(string value)
{
// We all know that the UserName and HostName for logging in can be inferred from email address
// So it would be user-friendly to provide default value for these fields
var split = value.Split('@');
// Use method from base class
ProvideDefaultValue("UserName", split[0]);
ProvideDefaultValue("HostName", "imap." + split[1])
// With a little magic, these will show up when user begins to input these field
}
}
// Now we will only need one line of code to connect all the pieces
var account = HyperConsole.ReadObject<Account>();
// Then magically, the console will interactively ask user to input each fields,
// do validation with validator class
// and handle some special logic as defined by handler class
// Cool!
// Let's say that we have a class
public class Account
{
public string HostName { get; set; }
public int Port { get; set; }
public string Email { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public bool UseSSL { get; set; }
public AuthMethod AuthMethod { get; set; }
public bool IsPrimary { get; set; }
}
// And we want to make a console application to prompt user to inputting each fields
Console.Write("Input value for field:");
// TODO: we want different behaviour for diffrent types
// TODO: we want default value
var value = Console.ReadLine();
// And we need validation
if (!IsValid(value))
{
// TODO: Tell user what's wrong
// TODO: Go back to Line 15 again
}
// TODO: And there's conversion
// And repeat all this for all fields
// Then we can finally get what we want
var account = new Account(/*all the sh*t from user*/);
// Imaging doing this over and over and over from project to project
// Just painful :(
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment