Tuesday, June 14, 2005

 

Moving to new blog

I've decided to move off of BlogSpot to a more technically-oriented blog site: http://bloggingabout.net/joshua. Feel free to subscribe, and as always, comments are welcome!

Tuesday, June 07, 2005

 

DataRow.ClearErrors doesn't quite cut it

As noted in my previous post, DataRow.ClearErrors isn't as useful as it could be, but seems to handle clearing errors quite reasonably as long as you can loop down to the DataRows in error. It's actually worse than I thought - ClearErrors will take the errors away from your row, but it doesn't trigger any events to tell anyone that the row has changed! So if you have UI bound to your DataSet, showing errors to the user, and then you call ClearErrors on the row, the component doesn't know and won't refresh itself unless you do something else. Reflector to the rescue... interestingly the property setter for DataRow.RowError and the DataRow.SetColumnError method both handle the events. Setting either a column or row error to null does remove the error. So my method to reset DataSet errors has been slightly expanded (pardon the absense of leading spaces - Blogger again...):

public static void ClearDataSetErrors(DataSet data)

{

if (data.HasErrors)

{

foreach (DataTable table in data.Tables)

{

foreach (DataRow row in table.GetErrors())

{

foreach (DataColumn column in row.GetColumnsInError())

row.SetColumnError(column, null);

row.RowError = null;

}

}

}

}


Wednesday, June 01, 2005

 

Arithmetic Rounding in .Net

Quiz time: what is the value of test after the following code runs:


decimal test = decimal.Round(12.345m, 2);


12.35, right? No - imagine my surprise to find that it returns 12.34!!

It turns out that .Net employs a type of rounding called "banker's rounding". When the rounding logic encounters a midpoint (like the 5 in my example), rather than always rounding up, it rounds to then nearest even number.

Of course, in my situation, I needed the traditional "arithmetic rounding" which always goes up (or away from zero) when a midpoint is encountered. Finding no such feature in .Net, I wrote my own. If you see any bugs in it, let me know!


public static decimal ArithmeticRound (decimal d, int decimals)

{

decimal power = (decimal)Math.Pow(10, decimals);

// Note: converting to positive number before calculating rounding, so that we don't need separate logic for negative number handling

decimal floored = (decimal.Floor((Math.Abs(d) * power)) / power);

decimal result;

if (Math.Abs(d) >= floored + ((decimal)Math.Pow(10, - (decimals + 1)) * 5))

result = floored + (decimal)Math.Pow(10, -(decimals));

else

result = floored;

return result * Math.Sign(d);

}


BTW, .Net 2 provides an option on the Round function to use either type of rounding. See http://msdn2.microsoft.com/library/wxhaazw6(en-us,vs.80).aspx

 

Data validation in .Net using ErrorProvider and DataSet errors

I currently make heavy use of .Net typed datasets to handle the communication of data transactions between my business and UI layers. Recently we started getting into some serious application testing, and discovered various bugs where the application allowed bad data to be submitted and processed. Ooops - time for some validation checks! Hmmm... .Net datasets don't provide much validation out of the box!

Some of the validations were clearly business rules that had to be verified on a case-base-case basis. However, I still wanted to provide the user with an adequate error message, and ensure that keyboard focus was placed on the column in question, so that the user could easily correct it. I really didn't want the forms displaying the data to need to be aware of the specific business rules - there were already several instances in the application where business rules were getting checked in the form, resulting in duplicated code that was next-to-impossible to verify with automated unit tests.

So, how can I perform rules validation outside of the form and yet still provide enough information to the form for an optimal user experience? I needed some way to indicate the error message as well as the dataset table/row/column on which the error was occurring back to the UI layer, so that it could deal with the error display and set focus to the appropriate control on the form.

DataSet errors quickly came to mind. DataTables have a feature that allows you to "set" an error on a DataRow and/or DataColumn, identifying that the row or column is in error. The interface is quite simple: just call the DataTable.SetColumnError function, identifying error text and a column. If you want to mark a row as having an error, but don't want to identify a specific column, set the RowError string property on the DataRow.

OK - so it's easy enough to identify a row or column as having an error; what about making use of that information? Yes, you can programmatically identify the rows/columns in error on a dataset. In my case I needed to locate the errors and set focus to the appropriate cell in a C1TruDBGrid. It's not as straight-forward as I would like, but here's the code that I used ( sorry about the indentation - for some reason Blogger is stripping out html space characters, losing the indentation):

if (data.HasErrors)

{

// Loop through data tables, determine if we have a table bound to this grid

foreach (DataTable table in data.Tables)

{

if (table.HasErrors)

{

if (this.DataMember == table.TableName this.DataMember.EndsWith("." + table.TableName))

{

int i = 0;

foreach (DataRow row in table.Rows)

{

errorProcessed = this.setFocusToErrorOnRow(row, i);

if (errorProcessed)

break;

i++;

}

if (errorProcessed)

break;

}

}

}

}


Ideally the DataSet would provide a method to enumerate all errors, but no such luck. At least it tells us whether there are errors or not, so we can decide whether to go any further. For each DataTable, we check whether it has errors or not, and if so, loop through the rows and process each row. The row processing is hidden in setFocusToErrorOnRow, which loops through the results of DataRow.GetColumnsInError, a method that returns any columns on the row that have errors. Now that we have the rows and columns, we can do some magic to find them on the grid (sigh...), and finally set the focus!

Another feature of DataSet errors is the integration with ErrorProviders. C1TrueDBGrid automatically detects the error and puts a nice little red icon on the column or row in error, making it obvious to the user where the problem is.

One more thing: when revalidating a dataset, it is important to clear any errors previously added to it. You can do that by calling ClearErrors on the row. Would be nice to have that at the dataset level, but oh well; a few lines of code does the trick:

// Clear all existing errors

if (data.HasErrors)

foreach (DataTable table in data.Tables)

foreach (DataRow row in table.GetErrors())

row.ClearErrors();



Problem is, ClearErrors doesn't trigger anything that I can find back to the dataset to tell databound components to remove UI indications of the error. One more problem to figure out...

This page is powered by Blogger. Isn't yours?