Tuesday, November 29, 2016

Microsoft MVP Summit 2016

I've been a Microsoft MVP since 2003. I still consider myself basically a C# MVP, but Microsoft has rolled up a lot of developer categories under one umbrella now: Visual Studio and Development Technologies. As you may know, being a Microsoft MVP means helping the community, in my case, the .NET community of developers. I do this with my blog posts and the many questions that I try to answer on the MSDN forums: https://social.msdn.microsoft.com/Forums/vstudio/en-US/home.

Many of my readers have probably heard of the annual Summit in which a few thousand MVPs from around the world descend on the Microsoft campus for 4-5 days of geekiness. I have attended every Summit since I've been an MVP. This year's Summit was interesting though ... since Microsoft has gone Open Source with most of .NET's source code, not everything at the Summit this year was under NDA (Non-Disclosure Agreement). It was hard to keep track of what was and what wasn't, so I decided it was easier just to not talk about anything when I got back.

But we *were* told this: that a lot of what we were hearing about at the Summit would become public the following week at the Connect conference in NYC. And so it did ... the conference was streamed live at the time, but the videos are available at https://connectevent.microsoft.com/ ... watch and be amazed!

On the last day that I was at the Summit, I participated in the MVP Hackathon, where we get to write code and explore new features alongside Microsoft engineers. Kinda geeky and fun!  Here's a public link about the event: https://blogs.msdn.microsoft.com/webdev/2016/11/22/mvp-hackathon-2016/  I'm not in the group picture that was taken at the end of the day because I had to leave after lunch (pizza!) to drive home. It's not a long drive (100 miles), but I wanted to get home before dark (and the sun sets early this time of year) ... I really don't like to drive at night. During the Hackathon, I was learning how to write extensions to Visual Studio ... not a new thing, but something that I hadn't done before, so it was new to me. The other people in my group doing extensions wrote some awesome stuff: Nico Vermier, David Gardiner, Cecilia Wiren and Terje Sandstrom. Their stuff is all on GitHub, so take a look at the Hackathon link above and check out some of the great extensions that they wrote. I never finished my extension ... because I had to leave early and didn't really have anything specific in mind that I wanted to accomplish, I just wanted to learn how to do it. I should have finished my experimentation when I got home, but life and work get in the way sometimes. ;)

Happy coding ...

Sunday, October 09, 2016

OneDrive & Group Policy

This is another departure from my normal blog posts (which are usually about .NET and/or C#), but I had a problem with OneDrive last week and I found a way to fix it, which I will share with you.

My “old” computer is a Dell Venue 11 Pro running Windows 8.1. It’s been rather buggy since I bought the thing several years ago (especially the docking station which needs frequent “re-boots”), so I had attributed my issues to this inherent problem … but I was wrong. 

I noticed last week that OneDrive wasn’t updating and in fact, the little Cloud icon wasn’t even appearing in my system tray. OneDrive hadn’t been running in days and I never noticed it (the Venue isn’t my main computer anymore, as I now have a Surface Pro 4 running Windows 10 … which was having no problems at all with OneDrive). I figured that all I had to do was reboot and all would be fine. It wasn’t. When I rebooted, I noticed this message appear:

I really didn’t know if this had anything to do with my problem with OneDrive, but it definitely did *not* look like a good thing. So, I took a look at the Event Log and all it said was:

The Group Policy Client service failed to start due to the following error:
The service did not respond to the start or control request in a timely fashion.” 

I went to look at the Services (choose “Administrative Tools” under Control Panel, and then open “Computer Management” … click on Services under “Services and Applications”) and sure enough, the Group Policy Client service was not running.

Well, at least it gave me something else to Google. When I did that, I saw other people mentioning this error in respect to OneDrive … or actually, to SkyDrive (the precursor to OneDrive). As it turns out, when I bought my Venue, OneDrive hadn’t come out yet … so I had SkyDrive. When OneDrive arrived (with a Windows Update I assume, I don’t remember), it automatically linked my existing SkyDrive to the new OneDrive … however, my SkyDrive folders still “lived” in my User folder (C:\Users\Bonnie\SkyDrive). And there was the reason I was having a problem … my “standard” User folder was not accessible due to this Group Policy Service not starting.

So, I found this YouTube video by Eugene Robertus who told of a way to fix it by making some changes to the Registry. It’s a long video but I watched it all and, as it turns out, the thing that Eugene says to check for was OK in my Registry.


Go to the Registry key \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SvcHost. In the right panel, double-click the “netsvcs” entry to bring up the Edit Multi-String dialog box. Note that I already have an entry in that list for “gpsvc”, which is the Group Policy Service. So why wasn’t that Service running?

… so, back to the drawing board (back to Googling, actually).I found a blog post by a guy who had the exact same problem (everything looked fine in the Registry, GP Service still not running): Group Policy Client Services Error and this guy had the solution. Apparently, it’s possible there’s a blank line in that list in the Registry that’s causing a problem. He said that he went to the bottom of the list and noticed that the cursor was on a new line after the last entry. Just backspace so that the cursor is after the last character of the last entry, click OK. You’ll get a “Warning” message about “empty strings” and it says the Registry Editor will remove all empty strings it finds in that entry. Click OK again, it gets fixed and you’re done. Reboot and voila! No more error message about the Group Policy Service!

And guess what … my OneDrive started working too!

And just for the heck of it, I thought I’d check it out on my Surface Pro 4 too … I didn’t have a problem with OneDrive, but I did notice that the Group Policy Service wasn’t running (apparently though, it doesn’t always run … it’s a triggered service), but I had the same issue with the Registry entry (empty strings) so that got fixed … who knows what else it might have affected.

Tuesday, August 30, 2016

More DataAccess - And You Thought I Was Done With It!

Back in 2009, I wrote a 3-part series of blog posts about DataAccess. You can find them here: Part I, Part II and Part III.  These posts are still relevant, even though they are old. Then, a year ago, I revisited that topic to add some additional things to my DataAccess class (implementing IDisposable and Transactions with TransactionScope). You can find that here: DataAccess Revisited

And now, I need to add something else that I’ve been meaning to add, because my DataAccess class is missing something important!! It was pointed out to me by a few people (via email) probably also about a year ago … it just took me awhile to get back to this. I hope it haven’t negatively affected too many people (probably not, or I would have had more complaints about it … or people figured it out themselves, all the while cussing me out silently)!!!

Anyway, that missing thing is TableMappings on the DataAdapter!! Basically, this needs to be added to the BBDataAccess class:

private void SetTableMappings(DataSet ds)
{
if (this.oAdapter.TableMappings.Count > 0)
return;

DataTable dt;
string TableName;
for (int i = 0; i < ds.Tables.Count; i++)
{
dt = ds.Tables[i];
if (dt.TableName.ToUpper().StartsWith("TABLE") == false)
{
if (i == 0)
TableName = "Table";
else
TableName = "Table" + i.ToString();
this.oAdapter.TableMappings.Add(TableName, dt.TableName);
}
}
}

And the existing FillData() method needs to have a call to SetTableMappings() before filling the DataSet, like this:

protected void FillData(DataSet ds, string StoredProcName)
{
this.oCommand.CommandText = StoredProcName;
this.SetTableMappings(ds);
this.oAdapter.Fill(ds);
}

That should take care of it! 

Happy Coding!  =0)

Saturday, July 23, 2016

PEMSTATUS & More With Reflection In .NET

Many, many, *many* years ago, when I was a Visual FoxPro developer, we could make use of a VFP function called PEMSTATUS. One of the things that function could tell you was whether or not a particular class/object contained a particular Property or Event or Method (hence the “PEM”) and other information about that P/E/M.

I’ve repurposed some of that functionality in .NET. Many of you may remember a post I wrote about 7 years ago, Reflection in .NET. That blog post was about using Reflection to dynamically instantiate a class using a string containing the name of the class. Well, we can also use Reflection for determining if a class/object contains a P/E/M, and we can then use Reflection for getting the value of a property or executing a method … all dynamically, just with a string containing the name.

Let’s say that we add some static methods to that MyReflectionClass from that 7-year-old post.

First, how about a PemStatus() method that will just tell us if a P/E/M exists? Here it is, along with two supporting methods:

// PemStatus checks for existence of a Property, Event or Method
public static bool PemStatus(object o, string name, string PEM)
{
    switch (PEM.ToUpper())
    {
        case "P":
            PropertyInfo pi = o.GetType().GetProperty(name);
            if (pi == null)
            {
                // GetProperty will only work on "real" properties ... those with get/set
                // Consequently, we want to look at Fields if we don't get a hit with GetProperty
                FieldInfo fi = GetFieldInfo(o, name);
                if (fi == null)
                    return false;
            }
            return true;
        case "E":
            EventInfo ei = o.GetType().GetEvent(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
            if (ei == null)
            {
                // GetEvent doesn't always find the Event, so we'll check the Fields for it in that case
                FieldInfo fi = GetFieldInfo(o, name);
                return fi != null;
            }
            return ei != null;
        case "M":
            MethodInfo mi = GetMethodInfo(o, name);
            return mi != null;
        default:
            return false;
    }
}
private static FieldInfo GetFieldInfo(object o, string name)
{
    FieldInfo fi = null;
    if (o is System.Windows.Forms.Control && !(o is System.Windows.Forms.Form))
        fi = typeof(Control).GetField("Event" + name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
    if (fi == null)
        fi = o.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
 
    // Base class fields are not always searched, so we need to look in each BaseType if fi is still null
    if (fi == null)
    {
        Type baseType = o.GetType().BaseType;
        while (baseType != null && fi == null)
        {
            fi = baseType.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
            baseType = baseType.BaseType;
        }
    }
 
    return fi;
}
private static MethodInfo GetMethodInfo(object o, string name)
{
    MethodInfo mi = null;
 
    mi = o.GetType().GetMethod(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) as MethodInfo;
 
    // Base class methods are not always searched, so we need to look in each BaseType if mi is still null
    if (mi == null)
    {
        Type baseType = o.GetType().BaseType;
        while (baseType != null && mi == null)
        {
            mi = baseType.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
            baseType = baseType.BaseType;
        }
    }
 
    return mi;
}

Now, what is this useful for? How about this: you might want to simply test for the existence of a P/E/M before deciding to do something else in your logic.  Let’s think of something that kind of makes sense. Let’s say that you’ve got an instance of a class (whether or not it’s been dynamically instantiated doesn’t matter) … let’s call it MyTest. You want to see if it has a Property called “TestIt”. I use the term “Property” loosely here, because it could also be a field (public, protected or even private).

Now, say your requirements are such that if MyTest has a TestIt property, then you want to get the value of that property and then see if MyTest contains a Method with the name contained in the TestIt property … and if so, execute that Method!  It’s actually pretty easy to do, but we’ll need some more static methods added to MyReflectionClass. Here they are:

// The next 3 methods, do stuff with the P/E/M:
// You can get the Property's value, execute the Event's handler, or execute the Method
public static object GetPropertyValue(object o, string name)
{
    PropertyInfo pi = o.GetType().GetProperty(name);
    if (pi != null)
        return pi.GetAccessors()[0].Invoke(o, null);
 
    FieldInfo fi = GetFieldInfo(o, name);
    if (fi != null)
        return fi.GetValue(o);
 
    return null;
}
public static void ExecuteEventHandler(object o, string name, EventArgs args = null)
{
    object eventKey = null;
    FieldInfo fi = GetFieldInfo(o, name);
 
    if (fi != null)
        eventKey = fi.GetValue(o);
 
    if (eventKey != null)
    {
        // This is for Events on non-Control objects
        if (eventKey is EventHandler)
            ((EventHandler)eventKey)(o, args);
        else
        {
            // This is for Events on Controls (such as a Button Click)
            EventHandlerList eventHandlerList = null;
            PropertyInfo pi = o.GetType().GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
            if (pi != null)
                eventHandlerList = pi.GetValue(o, new object[] { }) as EventHandlerList;
 
            if (eventHandlerList != null)
            {
                var eventHandler = eventHandlerList[eventKey] as Delegate;
                Delegate[] invocationList = eventHandler.GetInvocationList();
                foreach (EventHandler item in invocationList)
                {
                    item(o, args);
                }
            }
        }
    }
}
public static object ExecuteMethod(object o, string name, object[] parms = null)
{
    object result = null;
 
    try
    {
        MethodInfo mi = GetMethodInfo(o, name);
        if (mi != null)
        {
            // Let's add checks for parameters while we're at it
            ParameterInfo[] pi = mi.GetParameters();
            if ((pi.Length == 0 && parms == null) || (parms != null && pi.Length == parms.Length))
                result = mi.Invoke(o, parms);
            else
                result = string.Format("Parameter mismatch in method '{0}'. Method expected {1} parameter{2}, but received {3}.",
                    name, pi.Length, pi.Length > 1 ? "s" : "", parms == null ? "none" : parms.Length.ToString());
        }
        else
            result = string.Format("Method '{0}' does not exist");
    }
    catch (Exception ex)
    {
        result = ex.Message;
    }
 
    return result;
}

Here is the class we’ll be instantiating and using in our testing:

public class TestForBlog
{
    protected string TestIt = "DoIt";

    protected void DoIt()
    {
        Console.WriteLine("Did It!!");
    }
}

The class *does* contain a field called TestIt. It defaults to containing the string “DoIt”. And there *is* a method called DoIt defined in this class!! Here’s how we implement this test:

// As mentioned, this could have been instantiated via Reflection, but that's not the point of this particular blog post. 
// Read the older post, for which I provided the link, to see how that would work ...
TestForBlog MyTest = new TestForBlog();
 
// First see if the Property "TestIt" exists:
if (MyReflectionClass.PemStatus(MyTest, "TestIt", "P"))
{
    // It does, let's grab the value, see if there's a method with that name and execute it if there is
    string MethodToTest = MyReflectionClass.GetPropertyValue(MyTest, "TestIt").ToString();
    if (MyReflectionClass.PemStatus(MyTest, MethodToTest, "M"))
        MyReflectionClass.ExecuteMethod(MyTest, MethodToTest);
}

That’s all there is to it. Hopefully you may find this useful!!

Happy coding!  =0)

Wednesday, April 27, 2016

Transport RSA Key Containers

We have a Message Bus application where messages are passed all around, from one application instance to another. This message traffic can be intranet, inter-domain or internet. We keep the messages safe from prying eyes by using RSA encryption (public keys are stored in databases).

Now, the whole RSA encryption thing is a topic for another day (or just Google for more info, in case I never get around to writing that blog post). Today, I’m going to talk about what to do if you need to move one of your existing Message Bus deployments to another computer for some reason (like the computer is old or dying and you need to replace it with new hardware).

This new computer will not have your RSA KeyContainer. Creating a new KeyContainer with the same name doesn’t get you the same keys, obviously. All your messaging traffic comes to a screeching halt, because now the messages can’t be decrypted with these different keys.

The solution is to copy your RSA KeyContainer to the new computer. I have a little utility application that I wrote that will read the RSA KeyContainer and write it to an XML file. The utility will also read from an XML file and create an RSA KeyContainer from it. In the UI of the utility, simply click one button to save the KeyContainer to the XML file. Click another button to create a KeyContainer from the XML file.

Here’s the basic code you need to do that (you can write the UI part of it yourself):

private string KeyContainerName = "Your.KeyContainer.Name";
private string DefaultFileName = "YourKeyContainer.xml";
private string SavedFileName = "";
private RSACryptoServiceProvider KeyContainer;

// call these from one of the two button clicks:
private void GetKeyContainerSaveToFile()
{
if (this.GetRSAContainer())
{
// save key container to a file
this.WriteDataToFile(this.KeyContainer.ToXmlString(true), this.DefaultFileName);
if (this.SavedFileName != "")
// Display to UI: "RSA KeyContainer saved to " + this.SavedFileName;
else
// Display to UI: "CANCELLED! RSA KeyContainer NOT saved to file";
}
}
private void GetXmlCreateKeyContainer()
{
if (this.GetRSAContainer())
{
// read key container XML from file and save it to the container
string key = this.ReadDataFromFile(this.DefaultFileName);
if (key != "")
{
this.KeyContainer.FromXmlString(key);
// Display to UI: "RSA KeyContainer info retrieved from file " + this.SavedFileName + " and saved to machine KeyContainer";
}
else
// Display to UI: "CANCELLED! RSA NOT retreived from file and NOT saved to machine";
}
}

// this will get an existing Container, or create a new Container if none exists
private bool GetRSAContainer()
{
try
{
// retrieve the key container
CspParameters cp = new CspParameters();
cp.KeyContainerName = this.KeyContainerName;
cp.Flags |= CspProviderFlags.UseMachineKeyStore;
this.KeyContainer = new RSACryptoServiceProvider(cp);
return true;
}
catch (Exception ex)
{
// Display the Exception to UI
return false;
}
}

// Write to an XML file:
private void WriteDataToFile(string Data, string FileName)
{
// Configure file dialog box
Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
dlg.FileName = FileName;
dlg.DefaultExt = ".xml"; // Default file extension
dlg.Filter = "XML documents (.xml)|*.xml"; // Filter files by extension

// Show save file dialog box
Nullable<bool> result = dlg.ShowDialog();

if (result == true)
{
this.SavedFileName = dlg.FileName;
using (StringReader sr = new StringReader(Data))
{
using (TextWriter tw = new StreamWriter(dlg.FileName))
{
string s = null;
while (true)
{
s = sr.ReadLine();
if (s != null)
tw.WriteLine(s);
else
break;
}
}
}
}
else
this.SavedFileName = "";
}

// Retrieve from the XML file
private string ReadDataFromFile(string FileName)
{
// Configure file dialog box
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
dlg.FileName = FileName;
dlg.DefaultExt = ".xml"; // Default file extension
dlg.Filter = "XML documents (.xml)|*.xml"; // Filter files by extension

// Show open file dialog box
Nullable<bool> result = dlg.ShowDialog();

string xml = "";
if (result == true)
{
this.SavedFileName = dlg.FileName;
using (StreamReader file = new StreamReader(dlg.FileName))
{
xml = file.ReadToEnd();
}
}
else
this.SavedFileName = "";

return xml;
}


Saturday, March 26, 2016

DataBinding DataSets: Directly vs BindingSource

I’ve noticed some confusion over a blog post that I wrote many years ago. It can be found here: Fun With DataSets The post is about the various versions of DataRows (Proposed vs Current) and how a DataTable databound to a Control can sometimes not correctly report that DataSet.HasChanges() is true. The post was about how to get around that problem.

I was originally just going to add an Update to that post, for clarification, but decided that a new post would be better. (I will Update that old post, but only to add a link to this new one).

The clarification I want to make is that this appears to be an issue only if you DataBind directly to your DataTable, like this:

myDataGridView.DataSource = ds.Tables[0];  

However, I have not yet seen this happen when using a BindingSource, like this:

BindingSource bs = new BindingSource();
bs.DataSource = ds.Tables[0];  
myDataGridView.DataSource = bs;

To still play it safe, just in case, you could do the following (instead of the more “expensive” solution I suggested in my previous Fun With DataSets post):

// assuming the BindingSource is called "bs", and the DataSet is "ds"
bs.EndEdit();
if (ds.HasChanges())
{
    // process your DataSet changes
}

Why did I write that old post (back in 2009), when BindingSource had been introduced more than 3 years earlier? It had been new to .NET 2.0 and Visual Studio 2005, released in January 2006. One would assume that by 2009, developers would be using BindingSource all the time and would never have a reason to have to worry about this.  A little history of my background will explain this …

I started working with .NET 1.0 in April 2002 (it had been released only a month earlier). We had just begun a new startup company with plans to develop a new state-of-the-art .NET application. Along the way, we started developing our own “Framework” that would be used throughout our application. I was heavily involved with the “Framework” development. Unfortunately, 1.0 was a little buggy, so we were all glad when 1.1 came out a year later (with Visual Studio 2003). But, even 1.1 had its issues, one being the subject of these two posts. The CommitProposedChanges() method in the first post was born out of these issues.

Now, cut to 3 years later and the release of .NET 2.0 … well, by then I had written a full-blown “Framework” that had DataBinding “built-in” to a lot of the base control classes, not to mention a lot of new developers working on various applications who were used to using the “Framework” the way it was. I probably could have refactored the “Framework” classes to incorporate the use of BindingSource, but I’d have to be damn sure that it was backward compatible so as not to break existing code.  Nope … if it ain’t broke, don’t fix it! 

Now, judging from a lot of recent questions I’ve seen on various Forums, there are still plenty of people doing DataBinding the “old” way. So, even my old posts are still relevant! Gotta keep ‘em all happy!!  =0)

Saturday, February 13, 2016

Compare Speed of For vs Foreach

A bit more than 6 years ago, I posted Fun With DataSets, in which I wrote about the Proposed and Current versions of a DataRow. A code snippet in the post was iterating through each DataSet’s collection of DataTables and each DataTable’s collection of DataRows. I was using for loops. Years later, I have 30 comments on that post, but in the most recent thread of comments, we debated the merits of using for loops or foreach loops … which methodology is faster?

I had been under the impression, since way back in 2002 and the .NET 1.1 days, that the for was faster. It may have been true for 1.1 and I, unfortunately, held on to that belief all these years!! But my recent tests prove that wrong for all subsequent versions of .NET … foreach is much faster, roughly 7 to 8 times faster! I was curious if it was always so, and I changed the TargetFramework in my test application to be able to test all the way back to the 2.0 Framework (couldn’t get back as far as 1.1). And yes, it was the same with the 2.0 Framework … foreach wins each time!

Here is the code I used for testing this:

// First, I retreived a few thousand rows out of a database. 
// Then merged them together in a loop about 7 times,
// resulting in a DataSet called dsGlobal, containing about 2 million rows
this.GetALotOfData();

// Then, in a button click:
private void button1_Click(object sender, EventArgs e)
{
// Be sure to add a using System.Diagnostics;
Stopwatch oWatch = new Stopwatch();
string msg = "";

if (this.Toggle)
{
// Time the for loop
this.Toggle = false;
oWatch.Start();
msg = this.WithFor();
oWatch.Stop();
}
else
{
// Time the foreach loop
this.Toggle = true;
oWatch.Start();
msg = this.WithForEach();
oWatch.Stop();
}
this.txtResults.Text += string.Format("{0} in {1} milliseconds\r\n", msg, oWatch.ElapsedMilliseconds;
}
private string WithFor()
{
// In my original blog post that I referenced above, I looped like this:
for (int nTable = 0; nTable < this.dsGlobal.Tables.Count; nTable++)
{
for (int nRow = 0; nRow < this.dsGlobal.Tables[nTable].Rows.Count; nRow++)
{
if (this.dsGlobal.Tables[nTable].Rows[nRow].HasVersion(DataRowVersion.Proposed))
{
this.dsGlobal.Tables[nTable].Rows[nRow].EndEdit();
}
}
}
return "For";
}
private string WithForEach()
{
foreach (DataTable dt in this.dsGlobal.Tables)
foreach (DataRow row in dt.Rows)
{
if (row.HasVersion(DataRowVersion.Proposed))
row.EndEdit();
}
return "Foreach";
}

Each iteration I did was almost identical, time-wise, with for averaging right around 800 milliseconds and foreach averaging around 100 milliseconds, for almost 2 million rows (the DataSet contained only one DataTable). Here are the results:

 
Time using 1,943,936 Rows
For in 832 milliseconds
Foreach in 101 milliseconds
For in 768 milliseconds
Foreach in 102 milliseconds
For in 772 milliseconds
Foreach in 102 milliseconds
For in 858 milliseconds
Foreach in 103 milliseconds
For in 757 milliseconds
Foreach in 132 milliseconds
For in 750 milliseconds
Foreach in 102 milliseconds

Now, in the comment thread from that first post, my commenter was concerned about the way I was doing the for loop, thinking that there might be some overhead in having to check this.dsGlobal.Tables[nTable].Rows[nRow]  everytime through the loop. So I added another method to modify the for loop code, and tested it again:

private string WithForRowsCollection()
{
DataRowCollection rows;
for (int nTable = 0; nTable < this.dsGlobal.Tables.Count; nTable++)
{
rows = this.dsGlobal.Tables[nTable].Rows;
for (int nRow = 0; nRow < rows.Count; nRow++)
{
if (rows[nRow].HasVersion(DataRowVersion.Proposed))
{
rows[nRow].EndEdit();
}
}
}
return "For (rows)";
}

It didn't really make much difference. My original for loop tended toward 8 times slower, and the test with the modified for loop tends towards 7 times slower. Both sets of for loops (the original and the one using a row collection variable to iterate through), averaged between 700 - 800 milliseconds, with the first in the high 700's and the second in the low 700's.
 
Time using 1,943,936 Rows
For (rows) in 687 milliseconds
Foreach in 102 milliseconds
For (rows) in 670 milliseconds
Foreach in 112 milliseconds
For (rows) in 775 milliseconds
Foreach in 131 milliseconds
For (rows) in 675 milliseconds
Foreach in 102 milliseconds
For (rows) in 757 milliseconds
Foreach in 140 milliseconds
For (rows) in 763 milliseconds
Foreach in 103 milliseconds

Now, granted, we’re only talking about 700 to 800 hundred milliseconds here … but in any time-critical application, this could make a huge difference. And I’m not really talking about applications with User Interfaces … it’s not anything a user would notice at all. But, any kind of service application that runs standalone or in collaboration with other service applications (such as Windows Services types of applications that run server-side), it could be very important!

And now, I think I’ll go update my old Fun With DataSets post to include a link here and to post the new code!

Happy coding!  =0)