Monday, December 30, 2019

Create a Dictionary using LINQ

Dictionaries are useful to use in your code, and occasionally I need to look up a word I don't know!!  Oh wait ... that's a different kind of dictionary!  ;0)

Say that you have some data that you'd like to put into a Dictionary. One reason for using a Dictionary might be to make it easy to process one group of items at a time. Another reason might be for easily finding data based on the dictionary key. The data could be in a file or maybe it's been retrieved from TCP or from a database. It doesn't matter. Data that might look something like this:

MyFirstGroup:
item1

MySecondGroup:
item2
item3
item4

Or maybe delimited data, like this:

|CARCOMPANY=GeeksRide|CARNUMBER=C121|CARNUMBER=C122|CARCOMPANY=UberGeek|CARNUMBER=C133|

The above string may look a bit familiar to regular readers of my blog. It's similar to data coming from a fictional ride-sharing system that I "invented" in order to write this blog post about parsing data: https://geek-goddess-bonnie.blogspot.com/2019/09/parsing-data-with-metadata.html

Oh, and how about XML? Here's some XML (in this case, representing a DataSet):

<RideShareOwnerDataSet>
  <CompanyInfo>
    <CarCompanyID>1</CarCompanyID>
    <CarCompany>GeeksRide</CarCompany>
  </CompanyInfo>
  <CompanyInfo>
    <CarCompanyID>2</CarCompanyID>
    <CarCompany>UberGeek</CarCompany>
  </CompanyInfo>
  <CarInfo>
    <CarNumberID>1</CarNumberID>
    <CarCompanyID>1</CarCompanyID>
    <CarNumber>C121</CarNumber>
  </CarInfo>
  <CarInfo>
    <CarNumberID>2</CarNumberID>
    <CarCompanyID>1</CarCompanyID>
    <CarNumber>C122</CarNumber>
  </CarInfo>
  <CarInfo>
    <CarNumberID>3</CarNumberID>
    <CarCompanyID>2</CarCompanyID>
    <CarNumber>C133</CarNumber>
  </CarInfo>
</RideShareOwnerDataSet>

Of course, depending on the data, the processing in your application could be quite different. So, let's take a look at how we would handle these three very different types of data.

In all of these examples, I am making use of LINQ syntax to create the Dictionary. In two of these examples, I make use of LINQ's .Aggregate() function. Here is a good link that explains how .Aggregate works:

https://stackoverflow.com/questions/7105505/linq-aggregate-algorithm-explained

Lines of Data

For this first example, I put the data into a text file, that I called GroupAndItem.txt. We can use the File.ReadLines() to read each line of data and utilize the .Aggregate() function of LINQ to make it pretty easy to dump this data into a Dictionary:

string key = "";
var dcSectionItems = File.ReadLines("GroupAndItem.txt")
    .Aggregate(new Dictionary<string, List<string>>(), (a, s) => {
        var i = s.IndexOf(':');
        if (i < 0) a[key].Add(s);
        else a[(key = s.Substring(0, i))] = new List<string>();
    
        return a;
    });


And then we can show the data by iterating the Dictionary utilizing Console.WriteLine() for display.

foreach (var kvp in dcSectionItems)
{
    Console.WriteLine("{0}:", kvp.Key); // section name
    foreach (var item in kvp.Value)
    {
        Console.WriteLine("\t{0}", item); // item name
    }
}


Delimited Data

Rather than reading from a file this time, I simply used a string to hold the pipe-delimited data. In practice, this data could be coming in from TCP, a web service or a database. Notice that GeeksRide has 2 cars, while UberGeek has only one. Again, I make use of the LINQ .Aggregate() function to create the Dictionary:

string pipeCompanyCars = "|CARCOMPANY=GeeksRide|CARNUMBER=C121|CARNUMBER=C122|CARCOMPANY=UberGeek|CARNUMBER=C133|";

string[] entries = pipeCompanyCars.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
key = "";
var dcCompanyCars = entries
    .Aggregate(new Dictionary<string, List<string>>(), (a, s) => {
        var i = s.IndexOf("CARCOMPANY=");
        if (i >= 0) a[(key = s.Substring(11))] = new List<string>();
        else
        {
            i = s.IndexOf("CARNUMBER=");
            if (i >= 0) a[key].Add(s.Substring(10));
        }
        return a;
    });


And, again, show the data in the Dictionary the same way:

foreach (var kvp in dcCompanyCars)
{
    Console.WriteLine("{0}:", kvp.Key); // car company name
    foreach (var item in kvp.Value)
    {
        Console.WriteLine("\t{0}", item); // car numbers
    }
}



XML Data


I almost exclusively utilize XML with DataSets. Most of the time, the DataSets in my applications are Typed DataSets. But, every once in a while, I'll use a plain old DataSet for one-time uses. My first example with XML data here will simply use a plain old DataSet.

The XML I'm using could have just as easily been retrieved from an .xml file to fill the DataSet using ds.ReadXml("FileName.xml"), but here I am filling the DataSet from an XML string. Here's the string and the code to read it into a DataSet :

string xmlCompanyCars =
    "<RideShareOwnerDataSet>" +
      "<CompanyInfo>" +
        "<CarCompanyID>1</CarCompanyID>" +
        "<CarCompany>GeeksRide</CarCompany>" +
      "</CompanyInfo >" +
      "<CompanyInfo>" +
        "<CarCompanyID>2</CarCompanyID>" +
        "<CarCompany>UberGeek</CarCompany>" +
      "</CompanyInfo >" +
      "<CarInfo>" +
        "<CarNumberID>1</CarNumberID>" +
        "<CarCompanyID>1</CarCompanyID>" +
        "<CarNumber>C121 </CarNumber >" +
      "</CarInfo>" +
      "<CarInfo>" +
        "<CarNumberID>2</CarNumberID>" +
        "<CarCompanyID>1</CarCompanyID>" +
        "<CarNumber>C122 </CarNumber >" +
      "</CarInfo>" +
      "<CarInfo>" +
        "<CarNumberID>3</CarNumberID>" +
        "<CarCompanyID>2</CarCompanyID>" +
        "<CarNumber>C133 </CarNumber >" +
      "</CarInfo>" +
    "</RideShareOwnerDataSet >";

DataSet ds = new DataSet();
StringReader sr = new StringReader(xmlCompanyCars);
ds.ReadXml(sr, XmlReadMode.InferSchema);

If you use XML strings to fill DataSet a lot, instead of the above code, I recommend creating a handy Extension method, like the following:

// Extension methods for DataSet:
public static void FillWithXml(this DataSet ds, string XML)
{
    StringReader sr = new StringReader(XML);
    ds.ReadXml(sr, XmlReadMode.InferSchema);
    ds.AcceptChanges();
}

Here, you'd use it like this:

ds.FillWithXml(xmlCompanyCars);

Here's the rest of the code for this XML example:

// Now let's get that DataSet into a Dictionary!
// We do need a "dummy" DataTable for this to work
DataTable dt = new DataTable();
dt.Columns.Add("CarCompany");
dt.Columns.Add("CarNumber");

var dcCompanyCarsFromDataSet = ds.Tables[0].AsEnumerable()
    .Join(ds.Tables[1].AsEnumerable(),
        rowCompany => rowCompany.Field<string>("CarCompanyID"), rowCar => rowCar.Field<string>("CarCompanyID"),
        (rowCompany, rowCar) =>
        {
            DataRow newRow = dt.NewRow();
            newRow["CarCompany"] = rowCompany.Field<string>("CarCompany");
            newRow["CarNumber"] = rowCar.Field<string>("CarNumber");
            return newRow;
        })
    .GroupBy(rowKey => rowKey.Field<string>("CarCompany"), rowValue => rowValue.Field<string>("CarNumber"))
    .ToDictionary(rowKey => rowKey.Key, rowValue => rowValue.ToList());

Notice the syntax for accessing the columns of data in LINQ with a regular DataSet. You *must* specify the type of data with .Field<string>("ColumnName"). There is no way around this (unless you use a Typed DataSet ... more on that in my final example). Also notice that I did *NOT* use the .Aggregate() function here. I could not figure out how to do it that way (and I doubt that it can be done, but if any of my faithful readers want to give it a try ... go for it! Leave me a comment if you figure it out). But the .GroupBy() and the .ToDictionary() work just fine!

One more thing that you may notice, and that is the CarCompanyID and the CarNumberID. In the XML, you can clearly see that they are numbers, most likely an int. But, because I read the XML in to a regular DataSet (and even though I used XmlReadMode.InferSchema), it did not infer that those numbers were anything but a string. Which is OK in this case, but just something to be aware of. We could have added Tables and Columns to that DataSet before we read in the XML data, but why bother? If I wanted that, I'd use a Typed DataSet.

And use a similar foreach, as before, to iterate the Dictionary:

foreach (var kvp in dcCompanyCarsFromDataSet)
{
    Console.WriteLine("{0}:", kvp.Key); // car company name
    foreach (var item in kvp.Value)
    {
        Console.WriteLine("\t{0}", item); // car numbers
    }
}

XML Data and a Typed DataSet

To experiment with Typed DataSets if you've never used them, since we have available XML, you can use ds.WriteSchema("RideShareOwnerDataSet.xsd") method to create an .xsd file. Then add the .xsd file to your solution and run the MSDataSetGenerator to create a Typed DataSet. Here's one of my blog posts about how to do that:

https://geek-goddess-bonnie.blogspot.com/2010/04/create-xsd.html

The relevant (to this post) part of that blog post is this:

Simply add that .xsd to your DataSet project, right-click on the .xsd and choose "Run Custom Tool". This is what will generate the Typed DataSet for you. If that option doesn't show up in your context menu, choose Properties instead and type "MSDataSetGenerator" in the Custom Tool property. After that, any time you make a change to the .xsd in the XML Editor and save the change, the Typed DataSet gets regenerated.

I edited the .xsd to change the datatypes of the IDs to an int, instead of a string. Go ahead and do that if you'd like to.

// We still need a "dummy" table
DataTable dt = new DataTable();
dt.Columns.Add("CarCompany");
dt.Columns.Add("CarNumber");

var dcCompanyCarsFromTypedDataSet = dsRideShareOwner.CompanyInfo
    .Join(dsRideShareOwner.CarInfo,
        // Notice that we can use the Typed syntax for the columns now
        rowCompany => rowCompany.CarCompanyID, rowCar => rowCar.CarCompanyID,
        (rowCompany, rowCar) =>
        {
            // Use the Typed syntax here too
            DataRow newRow = dt.NewRow();
            newRow["CarCompany"] = rowCompany.CarCompany;
            newRow["CarNumber"] = rowCar.CarNumber;
            return newRow;
        })
    // The GroupBy still needs to use the untyped syntax, because these Rows are from the untyped "dummy" DataTable
    .GroupBy(rowKey => rowKey.Field<string>("CarCompany"), rowValue => rowValue.Field<string>("CarNumber"))
    .ToDictionary(rowKey => rowKey.Key, rowValue => rowValue.ToList());

You'll notice that the LINQ syntax is quite different for Typed DataSet versus non-typed. We do *not* have to specify Field<string> for the columns we are using, since we can use the Typed property to access the column data: rowCompany.CarCompanyID, instead of rowCompany.Field<string>("CarCompanyID").  Much cleaner.

And, as before, iterate the Dictionary:

foreach (var kvp in dcCompanyCarsFromTypedDataSet)
{
    Console.WriteLine("{0}:", kvp.Key); // car company name
    foreach (var item in kvp.Value)
    {
        Console.WriteLine("\t{0}", item); // car numbers
    }
}

So, that's it. It's been a fun experiment for me as I wrote this blog post. I hope that you, Dear Reader, can put this to good use.

Happy Coding!!  =0)

Saturday, November 23, 2019

Missing A Project/Assembly Reference

I have several solutions that need to reference assemblies in another solution. For many years, I used assembly/DLL references, because the other solution is our "Core Framework" solution, and I didn't want other programmers messing around with our framework classes. At another company that I worked at, that happened all too often. Then I'd have to go back in and fix all the bugs they put into *my* Framework classes!!! Without the projects being in the solution, it wasn't as likely that someone would go in and mess around with those classes.

But assembly references can cause issues too (such as outdated assemblies and problems with automated Builds, to name just two). So I finally relented and started adding the necessary Core Framework projects to each new solution and then any new projects for that solution could have project references to the Framework instead of assembly references. All was well for a while, but then several months ago, the problems began.

In a new solution that included Framework projects, I started getting "red squigglies" everywhere for one particular Framework project, showing the usual error when I moused over: "blah blah blah does not exist (are you missing an assembly reference?)". None of the other Framework projects have had problems, only this one. The using directive for that project's namespace had the squiggly and any class that was in that namespace also had squigglies. And Intellisense wouldn't work for any method calls, etc. And here's the weird part: with all those errors, everything still built fine on my computer (and on the Build server)!!

I'm using Visual Studio 2017. I was at version 15.8.something, which was not the latest version. So, I thought that I needed to update (because nobody else had the problem), so I'm now at the latest version (15.9.17) and it did NOT help! =0(

Googling for "Visual Studio 2017 missing project references" offered all kinds of suggestions. Clean the solution and build. Clean the solution and rebuild. Delete the obj folder and reopen VS. Add dependency references. Remove the project and add it back in. Remove the reference and add it back in. And many more that I don't even remember right now ... none worked!!!

I would periodically, drop my search for a fix and continue working for a while. Until the fact that I had no Intellisense started driving me nuts again and I'd search some more. This went on like this for months. Until I finally found something that worked!!!

Someone on some Forum (StackOverflow, I think) suggested unloading the problem project. The red squigglies suddenly disappeared! And Intellisense was back! The project reference is still there (because unloading doesn't affect the .sln at all, which is nice because I don't have to worry about checking it into source control, since there's no change, and it won't affect any of the other developers).

All you have to do is right-click the offending project in the Solution Explorer and choose "Unload Project" (it's down towards the bottom of the menu). The project remains displayed in the solution, but is marked as "(unavailable)". If you need to, you can always right-click it again and choose "Reload Project".

I have no idea why this works (I tried to Google to find an answer to that question), but after trying off-and-on for months to find a solution to my problem, I'm not going to "look a gift horse in the mouth", as they say!

Now I can continue with my Happy Coding! =0)




Sunday, November 03, 2019

Printing Your WinForm

Many developers still use WinForms (Windows Forms), and I frequently see questions on the Forums about how to print your Forms. I think this type of question might be asked by developers wanting to have a printed version of their forms, with data filled in, to maybe show to a prospective customer. Or, maybe the application contains an Invoice Form with customer data to be printed to include with a customer purchase. I'm sure there are lots of reasons for wanting this functionality.

I have seen many examples of how to do this when Googling, and some work better than others. Some of the issues I've seen include not taking into account multiple monitors ... it only works with your main laptop/tablet screen, or the image ends up spanning your laptop screen plus all of your monitors (I use two external monitors ... imagine three screens crammed onto one page)! Not exactly what you're looking for, is it?

After a bit of trial-and-error, cobbling together bits and pieces from various other sources that I've found with Google, I've come up with this. It works pretty well, I think. Call this PrintForm() method from a button click:

public void PrintScreen()
{
System.Drawing.Printing.PrintDocument oDoc = new System.Drawing.Printing.PrintDocument();
oDoc.PrintPage += new System.Drawing.Printing.PrintPageEventHandler(oDoc_PrintPage);
PrintDialog pd = new PrintDialog();
pd.PrinterSettings = new System.Drawing.Printing.PrinterSettings();
pd.Document = oDoc;
oDoc.DefaultPageSettings.Landscape = true;

// print like this:
//if (pd.ShowDialog() == DialogResult.OK)
//{
// oDoc.Print();
//}

// for now, let's just use the print preview to demonstrate
PrintPreviewDialog ppd = new PrintPreviewDialog();
ppd.Document = oDoc;
ppd.ShowDialog();
}


The PrintPage EventHandler gets called for both the Print and the PrintPreviewDialog, so all the necessary code is in this event handler:

private void oDoc_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
{
// First, let's get the image of this running Form that we want to print
Bitmap formBitmap = new Bitmap(this.Width, this.Height);
this.DrawToBitmap(formBitmap, new Rectangle(0, 0, this.Width, this.Height));

// for clearer images
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;

// this bit of code is needed to figure out if we need to scale the bitmap to fit the page
// (there is an implicit conversion from int to float)
float neededWidth = this.Width;
float neededHeight = this.Height;

float availableWidth = e.PageBounds.Width;
float availableHeight = e.PageBounds.Height;

double neededRatio = neededWidth / neededHeight;
double availableRatio = availableWidth / availableHeight;
float fitRatio = 1;

if (neededRatio >= availableRatio)
fitRatio = availableWidth / neededWidth;
else
fitRatio = availableHeight / neededHeight;

// The Min size needs to be converted to int for the DrawImage() method.
int newWidth = Convert.ToInt32(Math.Min(neededWidth * fitRatio, availableWidth));
int newHeight = Convert.ToInt32(Math.Min(neededHeight * fitRatio, availableHeight));

e.Graphics.DrawImage(formBitmap, 0, 0, newWidth, newHeight);
}

Have fun and Happy Coding! =0)




Monday, September 30, 2019

Parsing Data With Metadata

Recently, I had worked on some parsing routines for messages received from another vendor. The messages come via TCP, but that's not relevant to this discussion. What I wanted to talk about is how a "generic" type of parsing mechanism ended up being useful for parsing data that is received in more than one format. The vendor initially only sent messages in a pipe-delimited fashion, which is how the parsing routine was originally designed. Then later, they added additional messages that were XML (which we could use to Fill a DataSet). We needed to be sure that the parsing routine would work with both types of messages, because we would still be receiving both types.

Let's pretend that this example comes from a ride-sharing system (it doesn't). So we have cars and drivers and we are receiving messages about the status of each car and its location.

A pipe-delimited message might come in looking like this:

|CARCOMPANY=GeeksRide|CARNUMBER=C121|STATUS=Active|OPERATORID=2401|OPERATORNAME=Bonnie|STATUSDATE=5/24/2018 12:54:40 PM|LASTCOORDTIME=5/24/2018 12:54:55|PHONENBR=555-555-0430|X=-122.650345|Y=45.535772|

As you can see, the data is coming into my application from a hypothetical company called GeeksRide.

An XML message might come in looking like this:

<RideShareDataSet>
  <RideShareCar>
    <CarCompany>GeeksRide</CarCompany>
    <CarNumber>C121</CarNumber>
    <Status>Active</Status>
    <X>-122.650345</X>
    <Y>45.535772</Y>
    <LastCoordTime>5/24/2018 12:54:55</LastCoordTime>
    <StatusDate>5/24/2018 12:54:40 PM</StatusDate>
    <OperatorID>2401</OperatorID>
    <OperatorName>Bonnie</OperatorName>
    <PhoneNBR>555-555-0430</PhoneNBR>
  </RideShareCar>
</RideShareDataSet>

For ease of comparison between the two methodologies, the XML in my example contains the same data as the pipe-delimited, although,  in reality, it could be different data and different schema (column names). In the case of this hypothetical ride-sharing system, we could be receiving messages from many different companies with different data and different schema. Each type of message in your application can have different metadata, if necessary, to correspond to the various companies schemas.

What does this metadata look like? Very, very similar to the pipe-delimited data! Whether the message is coming in as pipe-delimited or XML, this pipe-delimited metadata can be used for either! Here's an example of metadata that can be used to process the data coming in with the above schema:

|CARCOMPANY=Company|CARNUMBER=CarID|EVENTNO=TripID|STATUS=Status|OPERATORID=DriverID|OPERATORNAME=DriverName|RADIOID=|EVENTTYPE=|STATUSDATE=LastStatusDateTime|LOCATION=LocationName|X=Longitude|Y=Latitude|ADDRESS=Address|DIRECTION=heading|SPEED=speed|LASTCOORDTIME=LastGPSDateTime|PHONENBR=PhoneNumber|

Let me explain what this means: each pair of values between the pipes,  |XX=YY|, correspond to the name of the data coming in (XX) and the name of the column in our ride-sharing system's DataSet/DataTable (YY).  Pretty simple, right?

Note that the leading and trailing pipes are optional in both the metadata and pipe-delimited input data.

OK, so now some code.

First, to simulate an incoming message, I'll just create pipeInput and xmlInput string variables. Like so:

private string pipeInput = "|CARCOMPANY=GeeksRide|CARNUMBER=C121|STATUS=Active|OPERATORID=2401|OPERATORNAME=Bonnie|STATUSDATE=5/24/2018 12:54:40 PM|LASTCOORDTIME=5/24/2018 12:54:55|PHONENBR=555-555-0430|X=-122.650345|Y=45.535772|";

private string xmlInput =
    "<RideShareDataSet>" +
    "  <RideShareCar>" +
    "    <CarCompany>GeeksRide</CarCompany>" +
    "    <CarNumber>C121</CarNumber>" +
    "    <Status>Active</Status>" +
    "    <X>-122.650345</X>" +
    "    <Y>45.535772</Y>" +
    "    <LastCoordTime>5/24/2018 12:54:55</LastCoordTime>" +
    "    <StatusDate>5/24/2018 12:54:40 PM</StatusDate>" +
    "    <OperatorID>2401</OperatorID>" +
    "    <OperatorName>Bonnie</OperatorName>" +
    "    <PhoneNBR>555-555-0430</PhoneNBR>" +
    "  </RideShareCar>" +
    "</RideShareDataSet>";

And I'll also simulate the metadata with a string variable as well. Normally you'd get the metadata from a config file or from a database:

private string pipeMetadata = "|CARCOMPANY=Company|CARNUMBER=CarID|EVENTNO=TripID|STATUS=Status|OPERATORID=DriverID|OPERATORNAME=DriverName|RADIOID=|EVENTTYPE=|STATUSDATE=LastStatusDateTime|LOCATION=LocationName|X=Longitude|Y=Latitude|ADDRESS=Address|DIRECTION=heading|SPEED=speed|LASTCOORDTIME=LastGPSDateTime|PHONENBR=PhoneNumber|";

One more variable we'll need is a Dictionary to store the metadata in. I call it a SchemaMap:

/// <summary>
/// The key is the data name of the incoming data, as defined in the Metadata (pipeMetadata)
/// The value is the column name of the DataTable where we store the Parsed data, as defined in the Metadata
/// </summary>
Dictionary<string, string> SchemaMap = new Dictionary<string, string>();

To test all this, just call this Test method (you can set a breakpoint at the end and look at the two DataTables that now contain the data parsed from the two different input messages):

public void TestParsingMethods()
{
    this.CreateSchemaMap(pipeMetadata);

    DataTable dtWithPipeInput = new DataTable();
    this.ParsePipeDelimited(dtWithPipeInput, pipeInput);
    
    DataTable dtWithXmlInput = new DataTable();
    this.ParseXml(dtWithXmlInput, xmlInput);
}

Let's start with the CreateSchemaMap, which is very simple. It fills the dictionary with the metadata:

public void CreateSchemaMap(string schema)
{
    string[] names = schema.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
    this.SchemaMap = new Dictionary<string, string>();
    
    for (int i = 0; i < names.Length; i++)
    {
        string[] s = names[i].Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
        if (s.Length == 2)
            SchemaMap.Add(s[0].ToUpper(), s[1]);
    }
}

The two Parsing methods, which I'll show first, both call a common method that puts the data into the DataRow with the proper column names, based on the metadata in the SchemaMap dictionary:

public void ParsePipeDelimited(DataTable dt, string pipeMessage)
{
    // If we're passed an undefined DataTable (one with no columns), we need to add string columns
    // as we have no way of accurately determining the type of data.
    // Typically, we'd already have defined a DataSet/DataTable with column datatypes.
    if (dt.Columns.Count == 0)
        foreach (KeyValuePair<string, string> kvp in this.SchemaMap)
        {
            if (dt.Columns.Contains(kvp.Value) == false)
                dt.Columns.Add(kvp.Value);  // default data type is string
        }
    
    // We'll be parsing data from the Message into this DataRow
    DataRow row = dt.NewRow();
    dt.Rows.Add(row);
    
    string[] nameValue = pipeMessage.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
    string schemaKey, columnName;
    for (int i = 0; i < nameValue.Length; i++)
    {
        string[] s = nameValue[i].Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
        schemaKey = s[0].ToUpper();
        if (s.Length == 2 && SchemaMap.ContainsKey(schemaKey))
        {
            columnName = this.SchemaMap[schemaKey]; // get column name from dictionary
            this.ParseToDataRow(row, columnName, dt, s[1]);
        }
    }
}
public void ParseXml(DataTable dtParsed, string xmlMessage)
{
    // If we're passed an undefined DataTable (one with no columns), we need to add string columns
    // as we have no way of accurately determining the type of data.
    // Typically, we'd already have defined a DataSet/DataTable with column datatypes.
    if (dtParsed.Columns.Count == 0)
        foreach (KeyValuePair<string, string> kvp in this.SchemaMap)
        {
            if (dtParsed.Columns.Contains(kvp.Value) == false)
                dtParsed.Columns.Add(kvp.Value);  // default data type is string
        }
    
    // We'll be parsing data from the Message into this DataRow
    DataRow row = dtParsed.NewRow();
    dtParsed.Rows.Add(row);
    
    // Now, let's read the XML into a DataSet:
    // I suggest making this into an Extension method, it's pretty handy to have.
    DataSet dsFromXml = new DataSet();
    StringReader sr = new StringReader(xmlMessage);
    dsFromXml.ReadXml(sr, XmlReadMode.InferSchema);
    dsFromXml.AcceptChanges();
    
    // Most likely only one Table, but just in case
    foreach (DataTable dt in dsFromXml.Tables)
    {
        if (dt.Rows.Count == 0)
            continue;
        
        DataRow rowFromXml =  dt.Rows[0];
        string columnName;
        foreach (string schemaKey in this.SchemaMap.Keys)
        {
            columnName = this.SchemaMap[schemaKey]; // get column name from dictionary
            foreach (DataColumn dcFromXml in dt.Columns)
            {
                if (dcFromXml.ColumnName.ToUpper() == schemaKey)
                {
                    this.ParseToDataRow(row, columnName, dtParsed, rowFromXml[schemaKey].ToString());
                    break;
                }
            }
        }
    }
}

Note the comments at the beginning of each of those methods. I used Typed DataSets extensively, so I don't need to be adding any columns to a "generic" DataTable. But, you don't have to have a Typed DataSet to have a DataTable with pre-defined columns and datatypes.

And lastly, the common method that they both call:

private void ParseToDataRow(DataRow row, string columnName, DataTable dt, string data)
{
    foreach (DataColumn dc in dt.Columns)
    {
        if (dc.ColumnName.ToUpper() == columnName.ToUpper())
        {
            if (dc.DataType == Type.GetType("System.Double"))
            {
                double lat;
                if (double.TryParse(data, out lat))
                {
                    if (data.Contains('.'))
                        row[dc] = lat;
                    else
                        row[dc] = lat / 1000000;
                }
            }
            else if (dc.DataType == Type.GetType("System.Int32"))
            {
                int val;
                if (int.TryParse(data, out val))
                {
                    row[dc] = val;
                }
            }
            else if (dc.DataType == Type.GetType("System.DateTime"))
            {
                DateTime date;
                if (DateTime.TryParse(data.Replace('/', ' '), out date))
                {
                    row[dc] = date;
                }
            }
            else
            {
                if (string.IsNullOrWhiteSpace(data) == false)
                    row[dc] = data;
            }

            break;
        }
    }
}

So, now you've seen how the same metadata and (at least part of) the same code can be used to parse data out of two very different types of data. I hope that you, dear Reader, can find some use for this.

Happy Coding! =0)