Wednesday, March 19, 2008

Working with DataTables

I recently had to write a method that would take a single row from a DataTable and expand that entry based on a start and end date and then determine which rows intersected with a range of dates.

The process to accomplish this seemed fairly straight forward:

  • Clone the original table.
  • Populate the new table with the correct number of entries.
  • Add the new rows back that fall within the range of dates.

 

Original Item:

ItemId StartDate EndDate Time
12345 3/5/2008 3/8/2008 8:00 AM

 

Expanded Items:

ItemId StartDate EndDate Time
12345 3/5/2008 3/8/2008 8:00 AM
12345 3/6/2008 3/8/3008 8:00 AM
12345 3/7/2008 3/8/2008 8:00 AM
12345 3/8/2008 3/8/2008 8:00 AM

 

Expected Results for Range of 3/6/2008 to 3/8/2008

ItemId StartDate EndDate Time
12345 3/6/2008 3/8/3008 8:00 AM
12345 3/7/2008 3/8/2008 8:00 AM
12345 3/8/2008 3/8/2008 8:00 AM

 

When I originally started out with creating this method my thought was to create only the rows that would be needed. So I began writing a bunch of branching logic that would compare the start date and the range start, compare the range end and end date, etc.; trying to determine how many days needed to be created and what day to start with. It quickly became very complicated and difficult for me to determine what I was doing. So I went back to the drawing board and figured out that with the help of the DataTable and DataRow classes in .NET that I could just expand the entire original item and then just select the rows where that intersected with my date range.

 

Code

Here is version of the method that I ended up with, I basically just removed the business logic that was specific to our application.

 

        public DataTable ExpandRows(DataTable original, DateTime rangeStart, DateTime rangeEnd)

        {

            DataTable expanded = original.Clone();

 

            //Assumes only one row for demo purposes

            DateTime itemStart = DateTime.Parse(original.Rows[0]["StartDate"].ToString());

            DateTime itemEnd = DateTime.Parse(original.Rows[0]["EndDate"].ToString());

 

            int numDays = itemEnd.Subtract(itemStart).Days;

            for (int i = 0; i <= numDays; i++)

            {

                DataRow newRow = expanded.NewRow();

                newRow.ItemArray = original.Rows[0].ItemArray;

                newRow["StartDate"] = itemStart.AddDays(i);

                expanded.Rows.Add(newRow);

            }

 

            //select the rows that fall within the specified range of dates.

            DataRow[] addRows =

                expanded.Select(

                    string.Format("StartDate >= '{0}' AND StartDate <= '{1}'", rangeStart.ToShortDateString(),

                                  rangeEnd.ToShortDateString()));

 

            //Remove the original row,since we do not know if it falls within range.

            original.Rows.Remove(original.Rows[0]);

 

            //add expanded rows that fall within the range

            int numRowsToAdd = addRows.Length;

            for (int j = 0; j < numRowsToAdd; j++)

            {

                original.ImportRow(addRows[j]);

            }

 

            return original;

        }

Limiting Results

Because I was using this method on a search page and I wanted to limit the results, because if you have an item with a large start/end date span and as well as a large range start/end date span there cold be hundreds or thousands of rows in the table. To limit the results I used a technique that I found in the ASP.NET Forums that shows how to add a unique id to the table and only return the a specified number of rows. So here is the final code with the limit the number of rows. Below is the final code:

        public DataTable ExpandRows(DataTable original, DateTime rangeStart, DateTime rangeEnd, int maxRowCount)

        {

            DataTable expanded = original.Clone();

 

            //Assumes only one row for demo purposes

            DateTime itemStart = DateTime.Parse(original.Rows[0]["StartDate"].ToString());

            DateTime itemEnd = DateTime.Parse(original.Rows[0]["EndDate"].ToString());

 

            int numDays = itemEnd.Subtract(itemStart).Days;

            for (int i = 0; i <= numDays; i++)

            {

                DataRow newRow = expanded.NewRow();

                newRow.ItemArray = original.Rows[0].ItemArray;

                newRow["StartDate"] = itemStart.AddDays(i);

                expanded.Rows.Add(newRow);

            }

 

            //select the rows that fall within the specified range of dates.

            DataRow[] addRows =

                expanded.Select(

                    string.Format("StartDate >= '{0}' AND StartDate <= '{1}'", rangeStart.ToShortDateString(),

                                  rangeEnd.ToShortDateString()));

 

            //Remove the original row,since we do not know if it falls within range.

            original.Rows.Remove(original.Rows[0]);

 

            //add expanded rows that fall within the range

            int numRowsToAdd = addRows.Length;

            for (int j = 0; j < numRowsToAdd; j++)

            {

                original.ImportRow(addRows[j]);

            }

 

 

            if (maxRowCount > 0)

            {

                if (original.Rows.Count > maxRowCount)

                {

                    DataTable autoIds = new DataTable();

                    DataColumn column = new DataColumn();

                    column.DataType = typeof(Int32);

                    column.AutoIncrement = true;

                    column.AutoIncrementSeed = 1;

                    column.AutoIncrementStep = 1;

                    column.ColumnName = "AutoRowId";

                    autoIds.Columns.Add(column);

 

                    autoIds.Merge(original, true, MissingSchemaAction.AddWithKey);

 

                    DataTable truncated = original.Clone();

                    DataRow[] limitedRows = autoIds.Select(string.Format("AutoRowId<={0}", maxRowCount));

 

                    for (int z = 0; z < maxRowCount; z++)

                    {

                        truncated.ImportRow(limitedRows[z]);

                    }

 

                    return truncated;

                }

            }

 

            return original;           

        }

 

 

Lessons Learned

Here are some of the things that I learned along the way.

  • Sometimes you need to step back and reassess the path you have initially chosen, it may not always be the best one. And in this case I came up with something that ended up being much simpler and more efficient.
  • The importance of Unit Tests. I have been experimenting with using Test Driven Development (TDD) recently. So I was decided to switch my approach I was able to feel confident that the code was still working as expected, because I had immediate feedback as I made changes.
  • You cannot directly insert a datarow from one table into another. If you try to do this you will get the error:  "This row already belongs to another table". Instead, use ImportRow to copy the row into another DataTable.
  • I was using a DataView to do some sorting on the DataTable like the following,

                DataView dataView = new DataView(original, string.Empty, "StartDate ASC", DataViewRowState.OriginalRows);

However, because I was removing the original rows and adding new ones, the table was missing the original rows and the newly added ones; Because as the name states, it was looking at the original rows in the table... I needed to use the setting DataViewRowState.CurrentRows to display the rows as they currently existed in the table.

Thursday, March 6, 2008

Silverlight 2 & ASP.NET MVC Preview 2

Today at Mix08, Microsoft officially announced and released the latest updates to Silverlight and the ASP.NET MVC Framework. You can download them via the following links:

Silverlight 2 - Includes the following:

  • Silverlight 2 Beta 1
  • Silverlight 2 SDK Beta 1
  • Silverlight Tools Beta 1 for VS 2008
  • KB949325 for VS 2008.

ASP.NET MVC Framework - Preview 2 

Make sure you also check out the ReadMe that describes some of known issues, etc. related to both Silverlight 2 and ASP.NET MVC Preview 2.

Monday, February 11, 2008

ASP.NET Wiki Beta

Scott Hanselman recently posted about a project his is working on ASP.NET Wiki Beta. The goal is "To provide a targeted, categorized, human-hand-edited, and living Wiki for finding answers to ASP.NET questions."  With the right input and moderation this sounds like it could become a great resource. Please check it out and add any relevant content that you have found over your span of working with ASP.NET.

Tuesday, January 22, 2008

Excluding Certain NUnit Tests

Another developer in my group wrote a group of Automation Tests that verify the conversion of some data within our database. He added these tests to our existing Testing assembly (. We are also using NAnt to run and validate the tests in our Testing assembly as part of our build process. However, we did not want this test to be included as part of the normal tests run during the build process. So to accommodate this we added a category of DatabaseConversion to the test fixture:

[TestFixture]

[Category("DatabaseConversion")]

Then I modified the exclude list for the NAnt task as follows:

<exec program="nunit-console.exe" basedir="${nunit.dir}" workingdir="${working.dir}">
  <arg value="${testassembly}" />
  <arg value="/xml:UnitTests.xml" />
  <arg value="/exclude:DatabaseConversion" />
</exec>

As I thought about this, I was worried about the fact that if someone forgets to put the appropriate category on a test or we decide that another category of tests needs to be excluded we would need to update the NAnt task again. So I went and did some digging in the NUnit documentation and found that there is an Explicit attribute that you can add at the TestFixture or Test level. The ExplictAttribute documentation describes this as follows:

"The Explicit attribute causes a test or test fixture to be ignored unless it is explicitly selected for running. The test or fixture will be run if it is selected in the gui, if its name is specified on the console runner command line as the fixture to run or if it is included by use of a Category filter.

An optional string argument may be used to give the reason for marking the test Explicit.

If a test or fixture with the Explicit attribute is encountered in the course of running tests, it is skipped unless it has been specifically selected by one of the above means. The test does not affect the outcome of the run at all: it is not considered as ignored and is not even counted in the total number of tests. In the gui, the tree node for the test remains gray and the status bar color is not affected."

So now the code looks like the following:

[TestFixture, Explicit]

[Category("DatabaseConversion")]

This is perfect for what we are trying to accomplish as it basically will exclude the test unless it is explicitly selected to be run. And I do not need to be worried about the exclude list for the NAnt task having .

Composition over Inheritance

Recently when walking through the design of some new features for an application, another developer asked: "Why should I use composition over inheritance?" I attempted to answer that question, and felt that I did not do as good of a job answering it as I would have liked. I was recently looking for something else on Phil Haack's blog and ran across this good article, Composition over Inheritance and other Pithy Catch Phrases.

I think that Phil does a very nice job of explaining the argument and the key point in why this question should be asked: "The goal is not to bend developers to the will of some specific patterns, but to get them to think about their work and what they are doing."  Please give this article a read for more details.

Wednesday, January 16, 2008

Getting Assembly Public Key

I recently needed to get the public key of an assembly, so I went straight to the GAC (Global Assembly Cache) located at %WinDir%\assembly, knowing that the public key token is a column in that view via the file explorer. However, the assembly that I wanted was not registered in the GAC. So I navigated to the location of the assembly on my hard drive and started poking around in the properties thinking the public key value might be in there, no luck. Then I remembered Lutz Roeder's Reflector. I opened the assembly using Reflector and there it was down in the details for that assembly. Another reason to have this very handy utility around.

 

ReflectorDetails

Thursday, January 3, 2008

A state aware generic list

 

A state aware generic list is a great post, showing you how to keep track of the state of the objects contained in a generic list. Mads uses a very simple technique for tracking the state of his list. Looks like something that can be very useful if you are rolling your own objects and you want to track the state of generic lists.