This posting is just a bit different from some of my other postings. This posting is the Online version of the Talk that I will give gave on Thursday 30 April 2009. You can download the source for my examples from here:
I had previously done a talk for the HB DNUG on MVC (
all about
www.mattslist.org.nz), and as part of that talk I had spoken about LinqToSql and Lambda Expressions expression usage with LinqToSql. However there is SO much more to the whole concept of what Linq can offer us, then what’s in LinqToSql.
Linq and Lambda expressions have become an integral part of my programming experience over the past 6 months; So much so that I realised they deserved their own dedicated “talk”. In addition to Linq and Lambda over the past 2 months I have also found myself really beginning to use Extensions as well, and I thought, if I roll in working with Extensions in addition to working with Linq and Lambda I will have a talk with some real meat to it, that might be able to help other people to program in a more elegant way!
So on to the talk
I needed a subject that had a problem domain that everyone could relate to, that most of us (if not all of us) have had to deal with in our programming career, perhaps repeatedly, and that could simply and elegantly be used to show exactly what these three new technologies bring to the table. And my answer was Parse Apart a Command Line.
I was working on my DB Upgrade Utility, to add support for using a command line (with optional parameters) to run a Database Upgrade process in the background. As I started to work on adding support for a command line I realised I needed to parse the command line in such a way it was easy to get to the settings. So I started to pull out my old code for parsing, and started with that. And once I started looking at my code, I started thinking about how I could refactor it a lot better!
I’ve been carrying this utility around with me in one form or another ever since I first wrote it all the way back in VB 4.0. I’ve converted / upgraded it every few years to the latest technology and added new bells and whistles. With this new feature for command line support that I have been thinking about for the past couple of years, now seemed like a good time to migrate it up to .Net 3.5.
As an added side benefit, while it wasn’t my main intention as I designed this talk, it might help those few people out there that are still learning how to “refactor” their code, because we are going to approach the process as one big refactor session.
So let’s start with the simplest case of parsing Command Line: Given a command of
CommandLineTest.exe -ParamNoValue /Param2=World -Param1=Hello NoLeadingChar=MyValue /ParamLongValue="Linq = Cool Programming" /?
We should see this UI.
OK, so here is the old school Parsing routine I used for this.
public static class OldSchoolParser {
/// <summary>
/// Parses the command line.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Dictionary<string , string></returns>
/// <remarks>
/// let's take in the array of strings that .Net has pre-parsed from the command line for us
/// The .Net framework preforms a split on the passed in commandline based on spaces
/// (it doesn't split on spaces that are contained within quotes)
/// </remarks>
public static Dictionary<string , string> ParseCommandLine ( string[] args ) {
var commandLine = new Dictionary<string , string>( );
foreach ( var item in args ) {
var arg = item;
// strip off the old school command line delimiters that may be present
if ( arg.StartsWith( "-" ) || arg.StartsWith( "/" ) ) {
arg = arg.Substring( 1 );
}
// check to see if we still have something to parse
if ( !string.IsNullOrEmpty( arg ) ) {
// determine the postion of the Key / Value delimiter
var pos = arg.IndexOf( '=' );
// if we have a delimiter
if ( pos > 0 ) {
// add a new entry to the dictionary, including both key and value
commandLine.Add( arg.Substring( 0 , pos ) , arg.Substring( pos + 1 ) );
} else {
// otherwise just add a new entry to the dictionary of just the key
commandLine.Add( arg , null );
}
}
}
return commandLine;
}
}
And here is the code for the form
public partial class Form1 : Form {
public Form1 ( ) {
InitializeComponent( );
}
// in this construction the Main proc has passed in the command lines
public Form1 ( string[] args )
: this( ) {
var commandLines = OldSchoolParser.ParseCommandLine( args );
foreach ( var item in commandLines ) {
var listViewItem = new ListViewItem( new string[] { item.Key , item.Value } );
listView1.Items.Add( listViewItem );
}
string concatenation;
if ( commandLines.ContainsKey("Param1") ) {
concatenation = commandLines["Param1"] + " ";
} else {
concatenation = "(Param1 not set) ";
}
if ( commandLines.ContainsKey( "Param2" ) ) {
concatenation += commandLines["Param2"] + " ";
} else {
concatenation += "(Param2 not set) ";
}
if ( commandLines.ContainsKey( "Param3" ) ) {
concatenation += commandLines["Param3"];
} else {
concatenation += "(Param3 not set)";
}
ConcatenationTextBox.Text = concatenation;
}
private void listView1_SizeChanged ( object sender , EventArgs e ) {
if ( listView1.Width >= 130 ) {
listView1.Columns[0].Width = ( listView1.Width - 30 ) / 2;
listView1.Columns[1].Width = ( listView1.Width - 30 ) / 2;
}
}
} -- Phase I in source
Taking a quick look at what we have here, the command line is passed to our form, which calls the parser routine. The Parser routine breaks up the Command Line into a Dictionary of Key / Value pairs (we could have used the NameValueCollection, but the Dictionary has more / better built-in methods available). We loop thru the dictionary putting the values into the list view, then finally we go looking for specific keys, concatenate the values they contain together and display them in a text box.
Let’s see if we can use some of the new tools to make this better.
Maybe Linq can let us dispense with the Dictionary altogether
My first thought is, when I look at this code, that except for the case where I want to iterate thru the commands, in a normal program I NEVER want to just loop thru them, in fact I usually want to look up values and do something specific with them. So let’s see if we can use Linq to accomplish that. Let’s refactor the part where I build up the concatenation string into its own proc and modify it there. We will use Linq to retrieve the values from the command line and insert them into the text box. We will look up and parse the value all in one go…
So here is the code we now end up with
// in this construction the Main proc has passed in the command lines
public Form1 ( string[] args )
: this( ) {
LinqTheParams( args );
FillInListView( args );
}
private void LinqTheParams ( string[] args ) {
string concatenation = "";
// Let's indicate that we want to look up all the items in the command line
// that start with a value of "/Param"
var linqResult = from arg in args
where arg.StartsWith( "/Param" )
select arg;
// Let's do the lookup we specified above and put the results into a list.
var matches = linqResult.ToList( );
// Now let's look thru the list and fill out our
foreach ( var arg in matches ) {
// determine the postion of the Key / Value delimiter
var pos = arg.IndexOf( '=' );
// if we have a delimiter
if ( pos > 0 ) {
// stick in our value
concatenation += arg.Substring( pos + 1 ) + " ";
} else {
concatenation += "(" + arg + " not set) ";
}
}
ConcatenationTextBox.Text = concatenation;
}
private void FillInListView ( string[] args ) {
var commandLines = OldSchoolParser.ParseCommandLine( args );
foreach ( var item in commandLines ) {
var listViewItem = new ListViewItem( new string[] { item.Key , item.Value } );
listView1.Items.Add( listViewItem );
}
} -- Phase II in source
and here is our result
Not quite what we want… in terms of result, but I’m not ready to give up on this approach yet. The problem here is that we retrieved EVERY command line argument that started with “/Param” which included the ParamLongValue argument. And we completely missed Param1 because that param started with “-” instead of “/”; Sure we could take care of these issues by making rules on our params and changing the inputs we are looking for, but remember we are trying not to solve this particular application (because really it’s of no use to us) we want to come up with a general solution that we can use in future projects. So we need to make it as general as we can.
First up, looking over the code, the part that breaks apart our argument is working fine, so let’s refactor that out of the mix that we are going to work with. Next let’s expand the list of things we are looking for, so we get the Param1 value.
private void LinqTheParams ( string[] args ) {
string concatenation = "";
// Let's indicate that we want to look up all the items in the command line
// that start with a value of "/Param"
var linqResult = from arg in args
where arg.StartsWith( "/Param" ) || arg.StartsWith( "-Param" )
select arg;
// Let's do the lookup we specified above and put the results into a list.
var matches = linqResult.ToList( );
// Now let's look thru the list and fill out our
foreach ( var arg in matches ) {
concatenation += GetArgValue( arg );
}
ConcatenationTextBox.Text = concatenation;
}
private static string GetArgValue ( string arg ) {
string concatenation = "";
// determine the postion of the Key / Value delimiter
var pos = arg.IndexOf( '=' );
// if we have a delimiter
if ( pos > 0 ) {
// stick in our value
concatenation += arg.Substring( pos + 1 ) + " ";
} else {
concatenation += "(" + arg + " not set) ";
}
return concatenation;
}
Well that’s better… but its also worse! We got Param1 now, but we also picked up the ParamNoValue as well, and Param1 and Param2 are out of order.
That’s not too hard to fix, let’s make our code look for the specific items we want.
// Let's indicate that we want to look up all the items in the command line
// that start with a value of "/Param"
var linqResult = from arg in args
where arg.StartsWith( "/Param2=" ) || arg.StartsWith( "-Param1=" )
select arg;
-- Phase III in source
That’s more like it, they are just out of order. But let’s not worry about that just yet. Let’s see if we can modify what we have to get rid of the need to “hard code” our lookup values.
That give us
private void LinqTheParams ( string[] args ) {
string concatenation = "";
var matches = GetArgs( args );
// Now let's look thru the list and fill out our
foreach ( var arg in matches ) {
concatenation += GetArgValue( arg );
}
ConcatenationTextBox.Text = concatenation;
}
private static List<string> GetArgs ( string[] args ) {
// Let's indicate that we want to look up all the items in the command line
// that start with a value of "/Param"
var linqResult = from arg in args
where arg.StartsWith( "/Param2=" ) || arg.StartsWith( "-Param1=" )
select arg;
// Let's do the lookup we specified above and put the results into a list.
var matches = linqResult.ToList( );
return matches;
}
Not bad, but let’s make it so we can pass in the params we are looking for…
private void LinqTheParams ( string[] args ) {
string concatenation = "";
var matches = GetArgs( args , "-Param1=" , "/Param2=" );
// Now let's look thru the list and fill out our
foreach ( var arg in matches ) {
concatenation += GetArgValue( arg );
}
ConcatenationTextBox.Text = concatenation;
}
private static List<string> GetArgs ( string[] args, string param1Key, string param2Key ) {
// Let's indicate that we want to look up the two items in the command line
// that match our Param Name
var linqResult = from arg in args
where arg.StartsWith( param1Key ) || arg.StartsWith( param2Key )
select arg;
// Let's do the lookup we specified above and put the results into a list.
var matches = linqResult.ToList( );
return matches;
}
We could make a minor fix in our Linq query, and get them to sort correctly.
var linqResult = from arg in args
where arg.StartsWith( param1Key ) || arg.StartsWith( param2Key )
orderby arg.Substring( 6 , 1 )
select arg;
and that gets us sorta working, but let’s be honest, this is too specific for our particular application when we are looking for a general solution.
The advantage to this approach of stringing together our lookups is of course that internally the look up happens in one loop thru the args structure, but the downside is you have to specify all your lookups in the signature for the proc. And if you need them in some sort of order you need to specify a specific orderby clause. We can get rid of those issues by looking up one item at a time. That does incur the cost of multiple loops, but it is a command line, shouldn’t be too long and if a machine can handle .Net 3.5 our loop won’t be big enough for the User to ever see a perceptible degradation in performance for a majority of applications.
So taking what we already have, let’s just refactor it a bit to get something a little more reusable!
private void LinqTheParams ( string[] args ) {
string concatenation = GetArgValue( args , "-Param1=");
concatenation += GetArgValue( args , "/Param2=" );
ConcatenationTextBox.Text = concatenation;
}
private string GetArgValue ( string[] args , string paramKey ) {
// Let's indicate that we want to look up the item in the command line
// that matches our Param Name
var linqResult = from arg in args
where arg.StartsWith( paramKey )
select arg;
var match = linqResult.First( );
return GetArgValue( match );
}
You may have noticed that with this refactor, since we are getting each item in one pass thru, I have changed the
to
I could have used
and that would have retrieved the correct value, but with the First statement, the query looks thru the args and STOPS as soon as it finds a match, returning that match. The ToList( ) function would grab the first match and then continue to loop thru the rest of the items to see if anything else matches. In our case, that isn’t necessary or very efficient. We have already taken the hit of looping multiple times thru the Args to find our values, let’s limit our loops to the smallest possible set of lookups.
Now that’s good so far, but I think we can do even better, make it read just a bit more clearly!
private void LinqTheParams ( string[] args ) {
string concatenation = GetArgValue( args , "Param1" , '-' );
concatenation += GetArgValue( args , "Param2" , '/' );
ConcatenationTextBox.Text = concatenation;
}
private string GetArgValue ( string[] args , string paramKey, char leadingChar ) {
// Let's indicate that we want to look up the item in the command line
// that matches our Param Name
var linqResult = from arg in args
where arg.StartsWith( leadingChar + paramKey + "=" )
select arg;
var match = linqResult.First( );
return GetArgValue( match );
}
-- Phase IV in source
Maybe Lambda Expressions can be used for cleaner looking code
OK, by now you should be seeing what we can do with Linq, let’s bring Lambda Expressions into the mix and see if we can’t make this look any better. For this next section we are just going to be talking about a very small part of the GetArgValue method:
var linqResult = from arg in args
where arg.StartsWith( leadingChar + paramKey + "=" )
select arg;
var match = linqResult.First( );
But these two little statements will have a lot to teach us.
So what exactly are Lambda expressions? the “Official” Microsoft definition is:
A lambda expression is an anonymous function that can contain expressions and statements, and can be used to create delegates or expression tree types.
But for now you can think of them as “inline functions”.
They are an extension of the concept of Anonymous Methods, but Lambda expressions do a bit more then that. Lambda’s as well as being able to do the heavy lifting for standing in for Anonymous Methods, can also be used to express Linq statements in a more “concise” and programmatic way.
Let’s take a look again, at what intellisense says are our available methods on a array of strings. To see exactly what Linq brings to the party, let’s go look at our program entry method, Main() and do a quick look see at it. First up, make sure the “using System.Linq;” if any is commented out. Here is what we get:
The standard list of things you can do with an array of strings (notice there is no scroll bar above). Now uncomment the “using System.Linq;” (or add it if needed), and try that again:
Wow, we got a whole bunch of new methods that are now available to us an array of strings just by adding the “using System.Linq;”. So many in fact we now have a scroll bar. And notice one of those options is First<>… Maybe that is related to the First that was used with the Linq Query. To find out, let’s go back to our GetArgValue method and bring up the intellisense list of methods on the linqResult…
Yep, same thing there. Is it possible that perhaps there is someway to execute the First<> function directly on the passed in args to this method? And the answer (in the not so elegant way I tried to use the Socratic method) is “Yes”. So let’s try this out on just the args parameter passed in:
well that tooltip looks a little different then what we usually see and it’s not so “obvious” what it is telling us. First up, what is the meaning of that “(extension)” note at the beginning of the line? Don’t worry just yet about that, it only means that the function is in fact NOT a native function to the array of Strings, but instead is being supplied by some other library, a little bit later on I’ll show you how you can make your own extensions.
Let’s move on to the next interesting thing that we will investigate right now. The tooltip tells us that it “Returns … first element … that satisfies a specified condition”, which sounds a lot like the sort of functionality that we are looking for, so the next question is “how do you supply that condition”?
As we start to use the Linq supplied First<> function we see we have two overloads.
Note: you don’t have to worry about the value(s) specified in the <> of the most of the Linq Extenstions, it is a Generic function and it will be able to determine what you are returning without your specifying it. Because you are telling the First<> to act upon an array of strings, it “knows” that a string will be returned so it fills that portion out for you, and behind the scenes makes itself “First<string>”
The first one, seem to take no condition search, and would just return the first thing it finds in the array, and that’s not enough for our purposes. So let’s look at the second one. That’s the one we want, and now it’s time to make a Lambda Expression.
private string GetArgValue ( string[] args , string paramKey, char leadingChar ) {
//var linqResult = from arg in args
// where arg.StartsWith( leadingChar + paramKey + "=" )
// select arg;
//var match = linqResult.First( );
var newMatch = args.First( arg => arg.StartsWith( leadingChar + paramKey + "=" ) );
return GetArgValue( newMatch );
}and if we run this, we get the same result
To be absolutely clear here while what we have just put in looks complete different from the previous statement, they are in fact absolutely equivalent! The underlying compiler creates the exact same CLR code. What happens behind the scenes is, due to the way that Lambda’s are parsed / constructed the complier can take the Lambda expression apart and use it to create the Linq expression. Let’s take a few moments to break that statement up.
Let's start by explaining how to “verbalize” a Lambda. we have a new operator introduced in that statement; the “=>” operator. Which is read as “goes to”, so in English you would say
“Select first Arg in Args, where Arg goes to Arg Starts With …”
Ok so that doesn’t come tripping off the tongue but at least all the other Kewl kids will know what you mean.
- The collection we are working on, in this case the string array args is used to assemble the from arg in args portion,
- The arg is supplied by the term used on the left side of the =>
- Entire right side of the Lambda “goes to” operator is used to construct the where clause
Finally one last minor point, I used arg => arg. … so that you could easily follow the refactoring, and there is nothing wrong with specifying the value that way, but out in the real world, most of the examples you’ll see use a single letter, instead of a meaningful name. Since the whole operation is contained in the one statement, I haven’t found this to be hard to understand, but you should use what ever convention makes the most sense to you. For our purposes today, I will go with the convention that is in most use…
So with a little more refactoring, here is what we get.
private string GetArgValue ( string[] args , string paramKey , char leadingChar ) {
var newMatch = args.First( a => a.StartsWith( leadingChar + paramKey + "=" ) );
return GetArgValue( newMatch );
}
Ok, now we still need to handle the case of that third parameter, right? so lets add the case for it and try that out
private void LinqTheParams ( string[] args ) {
string concatenation = GetArgValue( args , "Param1" , '-' );
concatenation += GetArgValue( args , "Param2" , '/' );
concatenation += GetArgValue( args , "Param3" , '/' );
ConcatenationTextBox.Text = concatenation;
}
OUCH! That’s not good (at all), we didn’t even get a standard .Net development error dialog, it just cut us off at the knees. OK, run it again but this time in full debug mode.
Ok at least this time we got can see the error, and it was “Sequence contains no matching element”, which of course makes sense, see (on purpose) our command line doesn’t have a Param3 specified. So we need to do some error handling, but we will try to be more elegant then a try catch.
Turns out there are a LOT of ways to solve this issue (and I think in the past 6 months I’ve done them all), in this case I’d recommend looking again at the list of Linq extension methods in our list to see if there might be something in there that we can use…
Well the first thing that pops up is the Contains method, we could first check to make sure the arg exists in the array, and only go get it if it is already in there. The code in that case would look something like this:
private string GetArgValue ( string[] args , string paramKey , char leadingChar ) {
if ( args.Contains( leadingChar + paramKey + "=" ) ) {
var newMatch = args.First( a => a.StartsWith( leadingChar + paramKey + "=" ) );
return GetArgValue( newMatch );
}
return paramKey;
}
But I have found I like using a different operator in cases like this: FirstOrDefault<>. While the above method works, it will require at least two loops, one to look, and then another to fetch. What the FirstOrDefault does for you is it either returns the first instance it finds, or in the case where it couldn’t find a match, it returns to you the default value for the object type you are doing the Lambda expression upon. For objects that would be a Null reference, for the string it is the empty string, and then for the other intrinsics (int, char, bool) it returns the value you would get if you declared a variable of that type and started to use it (int = 0, char = ‘’, bool=false).
So now we can make our code look like this.
private string GetArgValue ( string[] args , string paramKey , char leadingChar ) {
if ( args.Contains( leadingChar + paramKey + "=" ) ) {
var newMatch = args.First( a => a.StartsWith( leadingChar + paramKey + "=" ) );
return GetArgValue( newMatch );
}
return paramKey;
}
private static string GetArgValue ( string arg ) {
string concatenation = "";
// determine the postion of the Key / Value delimiter
var pos = arg.IndexOf( '=' );
// if we have a delimiter
if ( pos > 0 ) {
// stick in our value
concatenation += arg.Substring( pos + 1 ) + " ";
} else {
concatenation += "(" + arg + " not set) ";
}
return concatenation;
}
And finally we have what we came for, but we aren’t done yet, let’s see if we can make this even better using Extensions!
-- Phase V in source
Maybe we can use Extensions just like Linq to make it easy to re-use this stuff.
Now I think it is time to explore that question we asked earlier that I decided to ignore…
“…[W]hat is the meaning of that “(extension)” note at the beginning of the line? … it … means that the function is in fact NOT a native function to the array of Strings, but instead is being supplied by some other library”
Now we will explore the meaning of extension, what it is, and how you can use it.
Some more explanation of how Linq does what it does is the place to start, you may remember back in the Main() when we had commented out the using System.Linq; statement the list of methods we could execute on the string array was the normal ones you would expect to see, but when we had using System.Linq; active suddenly we had a whole bunch of new methods that the Linq library supplied to us that we could use against the string array, and in fact we used a couple of them.
But how did Linq “know” that it should offer these methods to us? The answer lies in Extensions. The Linq library has a whole set of functions and enhancements it can provide to us, and in order to make it possible to use these functions in a simple way, without requiring rebuilding the core .Net framework libraries (and also in order to support Dynamic Language support such as IronPython), Microsoft decided to extend the Framework with Extensions. This is a way of declaring a function in a library in such a way, that the compiler (and thus intellisense) can tell that it is meant to be used with a certain type of object. Further more there is no need to pass the object into the function, the compiler will do that hook up for you, AND it will add the function to the intellisense popup as if it were a function built into the original core library. This means that you can build your own extensions and have those functions show up as methods on user defined objects or even on the intrinsic variables.
Let’s start to see this in a real simple way. The project we started with already had a reference to a windows function assembly (a dll). The LongCloudUtilities assembly that had the OldSchoolParser in it. In addition there is an empty class in there called CollectionExtension. I’m going to add some simple code to class. In order for this to work, the class that we will be adding the extensions to MUST be a static class. We are going to add a static function to it.
namespace LongCloud.Utilities {
public static class CollectionExtensions {
public static int RandomMultiply ( int input, int maxMultiplier ) {
// let's get a random number that is less then the specified max
int multiplier = new Random( ).Next( 1 , maxMultiplier );
// let's return the input multiplied by that random multiplier.
return input * multiplier;
}
}
}
Pretty simple thing, now let’s modify our form to see what we can get out of it. We’ll just make a temporary modification of the place where we stick our 3 params into the textbox.
public Form1 ( string[] args )
: this( ) {
//LinqTheParams( args );
int myNum = 5;
int val = CollectionExtensions.RandomMultiply( myNum , 125 );
ConcatenationTextBox.Text = val.ToString( );
FillInListView( args );
}
That’s what I got from running it few times. Now, wouldn’t it be nice, because we like to randomly multiply integers all the time in our apps (Let’s just stipulate that we have a really weird problem domain), if we could make that function show up as a Method on the int type? Well we can, and it’s really easy. In order to make this work, we need to add only ONE word to our original static function: “this”. Place the “this” keyword in front of the first argument passed into the function, and our new extension will automatically be made available in intellisense to the Type of the first argument.
public static int RandomMultiply ( this int input , int maxMultiplier ) {
// let's get a random number that is less then the specified max
int multiplier = new Random( ).Next( 1 , maxMultiplier );
// let's return the input multiplied by that random multiplier.
return input * multiplier;
}
that simple change now means that as we modify our code in the form, we get a new method as part of the type integer.
If we add intellisense type commenting to the static method in our library, that will show up in the tooltip as well.
int myNum = 5;
ConcatenationTextBox.Text = myNum.RandomMultiply( 25 ).ToString( );
OK, that’s a silly example, but now you know everything you need to know to take our original solution and make it really easy to re-use. You see Linq has a whole bunch of functions defined just like our Silly example and in the case of Linq, most of those functions are set to show up for anything that is of type IEnumerable. Yes, that is correct, you can specify an extension should be applied against an object that implements an interface or that inherits from a class. And ANY instance of such an object will now be able to access your extension function as if it were a member method.
So let’s do a little refactoring and pull out our GetArgValue functions and move them into our Library. In our Library we end up with
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LongCloud.Utilities {
public static class CollectionExtensions {
public static string GetArgValue ( this string[] args , string paramKey , char leadingChar ) {
var newMatch = args.FirstOrDefault( a => a.StartsWith( leadingChar + paramKey + "=" ) );
if ( string.IsNullOrEmpty( newMatch ) )
newMatch = paramKey;
return GetArgValue( newMatch );
}
private static string GetArgValue ( string arg ) {
string concatenation = "";
// determine the postion of the Key / Value delimiter
var pos = arg.IndexOf( '=' );
// if we have a delimiter
if ( pos > 0 ) {
// stick in our value
concatenation += arg.Substring( pos + 1 ) + " ";
} else {
concatenation += "(" + arg + " not set) ";
}
return concatenation;
}
}
}
and our form becomes
public partial class Form1 : Form {
public Form1 ( ) {
InitializeComponent( );
}
public Form1 ( string[] args )
: this( ) {
LinqTheParams( args );
FillInListView( args );
}
private void LinqTheParams ( string[] args ) {
string concatenation = args.GetArgValue( "Param1" , '-' );
concatenation += args.GetArgValue( "Param2" , '/' );
concatenation += args.GetArgValue( "Param3" , '/' );
ConcatenationTextBox.Text = concatenation;
}
private void FillInListView ( string[] args ) {
var commandLines = OldSchoolParser.ParseCommandLine( args );
foreach ( var item in commandLines ) {
var listViewItem = new ListViewItem( new string[] { item.Key , item.Value } );
listView1.Items.Add( listViewItem );
}
}
private void listView1_SizeChanged ( object sender , EventArgs e ) {
if ( listView1.Width >= 130 ) {
listView1.Columns[0].Width = ( listView1.Width - 30 ) / 2;
listView1.Columns[1].Width = ( listView1.Width - 30 ) / 2;
}
}
}
-- Phase VI in source
Maybe we could put it all together to rewrite the OldSchoolParser?
To be honest with you, when I started down this road I hit this point and said, Hey, good enough! and for everything I needed it seemed like a great place to stop and wait for new requirements to come in, but… there is one distinct problem with this approach. Each time you go to grab a parameter from the command line, you have to loop thru the arguments each time with a potential of On. If all you are going to do is grab these few parameters once, well, good enough! But if you might potentially have a lot of command line arguments with lots of calls to values, well it’s just not optimized, so I submit for your approval the NewSchoolParser and Supporting Extensions
.
namespace LongCloud.Utilities {
public static class NewSchoolParser {
/// <summary>
/// Parses the command line.
/// </summary>
/// <param name="args">The args.</param>
/// <returns></returns>
/// <remarks>
/// let's take in the array of strings that .Net has pre-parsed from the command line for us
/// The .Net framework preforms a split on the passed in commandline based on spaces
/// (it doesn't split on spaces that are contained within quotes)
/// </remarks>
public static Dictionary<string , string> ParseCommandLine ( string[] args ) {
return ParseCommandLine( args , new List<Char>( ) { '-' , '/' } );
}
/// <summary>
/// Parses the command line.
/// </summary>
/// <param name="args">The args.</param>
/// <param name="leadingChars">The leading chars.</param>
/// <returns></returns>
public static Dictionary<string , string> ParseCommandLine ( string[] args , IEnumerable<char> leadingChars ) {
return ParseCommandLine( args , leadingChars , '=' );
}
/// <summary>
/// Parses the command line.
/// </summary>
/// <param name="args">The args.</param>
/// <param name="leadingChars">The leading chars.</param>
/// <param name="delimiter">The delimiter.</param>
/// <returns></returns>
public static Dictionary<string , string> ParseCommandLine ( string[] args , IEnumerable<char> leadingChars , char delimiter ) {
// the ToDictionary is a feature supplied to us by Linq.
return args.ToDictionary( s => ExtractKey( s , leadingChars , delimiter ) , s => ExtractValue( s , delimiter ) );
}
private static string ExtractKey ( string item , IEnumerable<char> leadingChars , char delimiter ) {
item = StripLeadingCharacter( item , leadingChars );
var pos = item.IndexOf( delimiter );
if ( pos > 0 ) {
return item.Substring( 0 , pos );
}
return item;
}
private static string ExtractValue ( string item , char delimiter ) {
var pos = item.IndexOf( delimiter );
if ( pos > 0 ) {
return item.Substring( pos + 1 );
}
return null;
}
private static string StripLeadingCharacter ( string item , IEnumerable<char> leadingChars ) {
foreach ( var chr in leadingChars ) {
if ( item.StartsWith( chr.ToString( ) ) ) {
return item.Substring( 1 );
}
}
return item;
}
}
}
public static class CollectionExtensions {
/// <summary>
/// Tries the get value for the specified key in the IDictionary. Returns <c>true</c>
/// if it finds the key and the value of the key is placed in the out result parameter.
/// If the key is not found the out result parameter is set to the default value for the type
/// and the function returns <c>false</c>
/// </summary>
/// <typeparam name="K">The type of the keys in the dictionary</typeparam>
/// <typeparam name="T">The type of value to retrieve from the dictionary</typeparam>
/// <param name="dictionary">The dictionary.</param>
/// <param name="key">The key.</param>
/// <returns>the value for the specified key, or if key is not present the default for the type of value</returns>
public static T ValueOrDefault<K, T>( this IDictionary<K , T> dictionary , K key ) {
return dictionary.ValueOrDefault( key , default( T ) );
}
/// <summary>
/// Returns the Value for the specified key in the IDictionary, or it returns the value that
/// was specified to the last parameter if the key is not in the dictionary
/// </summary>
/// <typeparam name="K">The type of the keys in the dictionary</typeparam>
/// <typeparam name="T">The type of value to retrieve from the dictionary</typeparam>
/// <param name="dictionary">The dictionary.</param>
/// <param name="key">The key.</param>
/// <param name="defaultValue">The default value to return if the key is not found in the dictionary.</param>
/// <returns></returns>
public static T ValueOrDefault<K, T> ( this IDictionary<K , T> dictionary , K key , T defaultValue ) {
T result;
if ( !dictionary.TryGetValue( key , out result ) ) {
result = defaultValue;
}
return result;
}
/// <summary>
/// Tries the get value for the specified key in the IDictionary. Returns true if it finds the key
/// other
/// </summary>
/// <typeparam name="K">The type of the keys in the dictionary</typeparam>
/// <typeparam name="T">The type of value to retrieve from the dictionary</typeparam>
/// <param name="dictionary">The dictionary.</param>
/// <param name="key">The key.</param>
/// <param name="result"><em>out</em> result.</param>
/// <returns><c>true</c> if key was found; <c>false</c> otherwise </returns>
public static bool TryGetValue<K, T> ( this IDictionary<K , T> dictionary , K key , out T result ) {
if ( dictionary.ContainsKey( key ) ) {
result = dictionary[key];
return true;
}
result = default( T );
return false;
}
}
and now our Form code to use this shiny new parser class.
namespace WindowsFormsApplication1 {
public partial class Form1 : Form {
public Form1 ( ) {
InitializeComponent( );
}
// in this construction the Main proc has passed in the command lines
public Form1 ( string[] args )
: this( ) {
var parsedCommandLine = NewSchoolParser.ParseCommandLine( args );
ConcatenationTextBox.Text = FillOurDisplayText( parsedCommandLine );
FillInListView( parsedCommandLine );
}
private string FillOurDisplayText ( Dictionary<string , string> parsedCommandLine ) {
string concatenation = parsedCommandLine.ValueOrDefault( "Param1" , "Hello" );
concatenation += parsedCommandLine.ValueOrDefault( "Param2" , "World" );
concatenation += parsedCommandLine.ValueOrDefault( "Param3" , " -- ( Linq, Lambda, and Extensions !!! ) " );
return concatenation;
}
private void FillInListView ( Dictionary<string , string> parsedCommandLine ) {
foreach ( var item in parsedCommandLine ) {
var listViewItem = new ListViewItem( new string[] { item.Key , item.Value } );
listView1.Items.Add( listViewItem );
}
}
private void listView1_SizeChanged ( object sender , EventArgs e ) {
if ( listView1.Width >= 130 ) {
listView1.Columns[0].Width = ( listView1.Width - 30 ) / 2;
listView1.Columns[1].Width = ( listView1.Width - 30 ) / 2;
}
}
}
}
-- Phase VII in source
Let me leave you with something to blow your mind!
In the code I’m using a positioning on a delimiter to break apart our value from the key when I parse the arguments.
private static string ExtractKey ( string item , IEnumerable<char> leadingChars , char delimiter ) {
item = StripLeadingCharacter( item , leadingChars );
var pos = item.IndexOf( delimiter );
if ( pos > 0 ) {
return item.Substring( 0 , pos );
}
return item;
}
private static string ExtractValue ( string item , char delimiter ) {
var pos = item.IndexOf( delimiter );
if ( pos > 0 ) {
return item.Substring( pos + 1 );
}
return null;
}
And yes I’m at least letting the user pick what character to use as the delimiter (because not all things use the “=”. ex. style=”height:50px;width:25%”). But wouldn’t be great if just like with passing in the delimiter to use, I could instead pass in a function that I want to be used by the code for determining where to split it apart? Since Linq uses all these things AND it lets you specify little functions for it to run as it does its job, wouldn’t it be nice if we could allow in our own extensions and code the ability for users to pass in their own Lambda expression that WE should use while we are processing things?
Well you can. It’s a bit complicated and beyond this talk but you could augment the NewSchoolParser to support…
var parsedCommandLine =
NewSchoolParser.ParseCommandLine(
args,
new List<Char>( ) { '-' , '/' },
a => a.IndexOf( "99" )
);
But that’s not in the examples provided with this tutorial, I leave that as an exercise for you! Or you could just wait a few days for me to finish up the code, and come back here to this blog and get the updated code then!
And so it goes…