65.9K
CodeProject is changing. Read more.
Home

Read Method with Generics & Delegate

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (10 votes)

Jul 8, 2016

CPOL

2 min read

viewsIcon

13007

In a console application, there is often the need to ask (and validate) some data from users. For this reason, I have created a function that makes use of generics and delegates to speed up programming.

Introduction

How many times do we write a console application where we need to insert some data? I write a lot of little utilities that ask for some input and automatize works for me. Every time I need to write the same code, with just a little variation.

So I write a method that relies on the Console Class of .NET Framework and do the following operation:

  1. Write message to user
  2. Read answer from user
  3. Try to parse the data acquired and check for constraint (if any)
  4. If parse fails, go to step 1

Background

Here is a typical example:

We have a little TcpClient that has to connect to different servers, each with a different Port. So we need to ask the user for a valid port and keep asking if the input does not meet the constraint.

int port = 0;
string line = "";
// repeat if any of this test fail
do
{
    // ask the user for data
    Console.Write("Port: ");
    // read user input
    line = Console.ReadLine();
    // if user "send" an empty string, default value
    // will be used
    if(string.IsNullOrWhitespace(line))
        line = "5000";
// try to parse data and to check if the result is
// between the permitted range
}while(int.TryParse(line, out port) && (5000 <= port && port <= 5100));

I don't know if it's just me, but sometimes I need to write this snippet 5 or 6 times in a row and this can be very annoying. So I have decided to rewrite the previous code into a little snippet which is more generic possible and that does the job for me:

delegate bool TryParseDelegate<T>(string str, out T val);

static T Read<T>(string msg, TryParseDelegate<T> parser, T _default = default(T))
{
    ConsoleColor oldColor = Console.ForegroundColor;
    int attempt = 0;
    string line;
    T value;

    do
    {
        // Error message (only after 1st attempt)
        if(attempt > 0)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("Wrong Value: Retry!");
            Console.ForegroundColor = oldColor;
        }
        attempt++;

        // Query User for Data:
        Console.Write(msg + ": ");
        line = Console.ReadLine();

        // Default value (empty line)
        if (string.IsNullOrWhiteSpace(line))
            return _default;
    }
    while (!parser(line, out value));
    return value;
}

Using the Code

I'm not completely satisfied with the use of a delegate, because I would have preferred a self-contained solution like the functors in "c", but with that snippet I can write really simple code like this:

var x = Read<double>("x value", double.TryParse);

which is really short, clear and let me write a lot of repetitive code faster and safer.

This snippet also works with a custom parser, like the one in the previous example:

var port = Read("Port[5000-5100]", (string str, out int val) =>
{
    return int.TryParse(str, out val) && (5000 <= val && val <= 5100);
}, 5000);

and can handle really complex scenario:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public static bool TryParse(string str, out Person val)
    {
        int age;
        val = null;
        var args = str.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries)
            .Select(arg => arg.Split(new[] { "=" }, StringSplitOptions.RemoveEmptyEntries))
            .Where(arg => arg.Length == 2)
            .ToDictionary(kv => kv[0], kv => kv[1]);
        if (args.ContainsKey("FirstName") &&
            args.ContainsKey("LastName") &&
            args.ContainsKey("Age") && Int32.TryParse(args["Age"], out age))
        {
            val = new Person
            {
                FirstName = args["FirstName"],
                LastName = args["LastName"],
                Age = Int32.Parse(args["Age"])
            };
            return true;
        }
        return false;
    }
}

static void Main(string[] args)
{
    ...
    // Read Person from Console
    var person = Read<Person>("Person", Person.TryParse);
    ...
}

Points of Interest

Generics and Delegates can be used to make more reusable and flexible code, letting us create the "skeleton" for a method that we can adapt to the type of the data we have to compute. Thanks to these methods, I can write safer code and be sure I will not let users insert unvalidated values.

History

  • v 1.0: First version, a lot of errors I guess!
  • v 1.1: Better management of defaults and errors (Thanks to PIEBALDConsult)