Tuesday, June 14, 2005
Moving to new blog
Tuesday, June 07, 2005
DataRow.ClearErrors doesn't quite cut it
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
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
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...