DataStream
LANGUAGES: C#,
VB.NET
ASP.NET VERSIONS: 1.0 | 1.1
Exploit
DataTable Events as Client Triggers
Use
DataTable events to perform validation and business processing based on the
changing of fields and rows.
By Brian
Noyes
It's
common in ASP.NET apps to use server control validation to catch changes being
made to data at the control level. ASP.NET validation controls perform
validation as close to the data input as possible, using client-side script if
possible, and server-side code regardless of whether client validation was
possible. This is the right approach for any kind of user input scenario, that
is, catch the problem as soon as you can.
However,
sometimes you'll want code validation or business processing logic close to
where the data lives in the application server, rather than close to where the
changes originate from the user. Using DataTable events, you can easily perform
pre- or post-change processing of fields and rows in a data set to write code
that is very much like using triggers in a database.
Get in the
Loop
The
DataTable class exposes a set of events that you can tap into to get
fine-grained control over changes being made to the contained data in the
table. There are three kinds of events, and each kind of event is exposed in pairs.
The events are fired when a column changes (ColumnChanging/ColumnChanged), when
a row is changed (RowChanging/RowChanged), or when a row is deleted
(RowDeleting/RowDeleted). The events named XXChanging are fired just before the
change is applied, and the events named XXChanged are fired just after the
change has been completed.
The
ColumnChanging and ColumnChanged events include an event argument of type
DataColumnChangeEventArgs, which includes three key properties: Column, Row,
and ProposedValue. Column is a reference to the DataColumn that is being
modified, Row is a reference to the DataRow that is being modified, and
ProposedValue contains the value that has been assigned to the field.
The row
events all include an event argument of type DataRowChangeEventArgs. This
object contains two properties of interest: Action and Row. The Action property
is an enumeration containing values of Add, Change, Delete, Commit, and
Rollback. This gives you an indication of why the event is firing for the row
in question. The Row property allows you to inspect the values of the fields if
needed through the ItemArray property on the DataRow class, or you can inspect
the RowState property to determine what the state of the row was before or
after modification.
To understand
exactly when each event will fire and what you can do in a handler for the
event, let's step through the three data modification scenarios (insert,
update, and delete). I will look at which events fire when for each of these
operations, and discuss the arguments that come with the event to allow you to
perform processing based on the change.
To get
started, you'll need a data set populated with some data and you'll need to
subscribe to the events in which you are interested. In this case, to keep the
code compact, I used the Data Access Application Block (DAAB) SqlHelper class
and a typed data set for the Customers table from Northwind (see Figure 1).
public void
LoadDataSet()
{
// Load the data set
SqlHelper.FillDataset(connString,CommandType.Text,
"SELECT * FROM Customers",m_ds,
new string[]{m_ds.Customers.TableName});
// Wire up the events
m_ds.Customers.ColumnChanging +=
new
DataColumnChangeEventHandler(OnColumnChanging);
m_ds.Customers.ColumnChanged +=
new DataColumnChangeEventHandler(OnColumnChanged);
m_ds.Customers.RowChanging +=
new
DataRowChangeEventHandler(OnRowChanging);
m_ds.Customers.RowChanged +=
new
DataRowChangeEventHandler(OnRowChanged);
m_ds.Customers.RowDeleting +=
new DataRowChangeEventHandler(OnRowDeleting);
m_ds.Customers.RowDeleted +=
new
DataRowChangeEventHandler(OnRowDeleted);
}
Figure
1. Fill the
Customers data set with the SqlHelper class and wire up event handlers for each
of the DataTable events.
For now,
each of the event handlers that is wired up in Figure 1 simply dumps some
information to the console so that you can examine the order that the events
fire. You can modify these handlers in the download code to experiment with
doing other processing based on the values accessible to you when the events
fire.
Handle
Change Events
When you
add a new row to a table from code, you typically create the new row, set the
values, and then add the row to the table, as follows:
CustomersDataSet.CustomersRow
newrow =
m_ds.Customers.NewCustomersRow();
// Populate the
fields that are non-null
newrow.CompanyName
= "FooBros";
newrow.CustomerID
= "FOOBR";
// Add it to
the table
m_ds.Customers.AddCustomersRow(newrow);
Each of
the lines of code above where a field value is being set through the property
on the typed data set is equivalent to setting the value through the DataRow
indexer in an un-typed data set. For each of these assignments, the
ColumnChanging event is fired, followed by the ColumnChanged event. Then, when
AddCustomersRow is called (equivalent to Rows.Add in an un-typed data set), the
RowChanging event is fired, followed by RowChanged.
Again,
each of the column-related events carries an event argument of type
DataColumnChangedEventArgs. You can get to the current value of the field using
the Column and Row properties, and the argument itself contains the value that
the field is being changed to as the ProposedValue property:
private void
OnColumnChanging(object sender,
DataColumnChangeEventArgs e)
{
string colName = e.Column.ColumnName;
object currVal = e.Row[e.Column.ColumnName];
object newVal = e.ProposedValue;
// Output to console...
}
Using
the ColumnName property of the Column property on the event argument, you could
easily set up a switch..case selection on the ColumnName, cast the current and
proposed values for the field to the appropriate type, and do either validation
or additional processing based on that change.
Trigger
Validation or Business Logic
For
validation, you would want to do your processing in the ColumnChanging event
handler, whether the change was triggered by an insert or an update. For
example, you could check that a data or money value was within an acceptable
range. Or perhaps for a more amorphous field type, such as a string field
containing delimited text or XML, you might transform the input value into some
other format to ensure consistent formatting in the underlying data. The
ProposedValue property is a read/write property, so you can change the value of
this property in the ColumnChanging event handler, and the value that you
change the property to will be the one that is used to set the value of the
field.
What if
you want to cancel the change? This gets a little dicier. Unfortunately, the
event arguments do not expose a Cancel property like many event arguments in
.NET, which allows you to cancel the firing of the event. So you are left with
a couple of options, none of which is completely satisfactory in all
situations. First and easiest, you can just set the value of the ProposedValue
property to the current value of the field in the ColumnChanging event:
e.ProposedValue
= e.Row[e.Column.ColumnName];
The only
problem with this approach is that if you are using the data set for updates to
the database, this will still result in the row being marked as changed,
resulting in a round trip to the database and potential triggering of other
change logic that may be inappropriate since the value did not in fact change
in a meaningful way.
Another
possibility is to throw an exception, which will cancel out event handling and
prevent the change from occurring. However, throwing an exception is an
expensive operation, so you wouldn't want to have this occurring often in your
processing. You would also have to make sure you have appropriate exception
handling in place.
Finally,
you could call RejectChanges on the row in the RowChanged event handler after
the change has been applied, but then you would also be throwing away any
changes to other fields in the row, which would be tough to work around in a
generalized way.
Another
way you can use the column events is to trigger post-change processing on other
data. If you have a dependency between a given field in a table and other
fields in other tables, or simply objects in your code, you could write code in
a ColumnChanged event handler to go make the appropriate changes to the
dependent objects or fields when a change was completed on a given field.
As
mentioned, when the insert is actually performed with the AddCustomerRow
method, two more events fire: RowChanging and RowChanged. For both of these
events and an insert operation, the Action property will be set to the Add
value of the enumeration. In the changing event, the RowState is still set to
Detached (the state any row is in when it is created but not yet added to a
table) and in the changed event the RowState is set to added so that it can be
used for filtering or database updates as desired. If you needed to include
logic that did auditing of changed rows by saving off the changes based on row
state, this would be where you could do so. By accessing the ItemArray property
of the row exposed through the event argument Row property, you could perform
different processing based on the Action and/or the RowState of the row.
When you
delete a row, no column events are fired, just the RowDeleting and RowDeleted
events with the Action property of the event arguments set to Delete. The
RowState will be set to whatever its current state is in the RowDeleting event,
and will be changed to Deleted in the RowDeleted event.
Updates
to a table work a lot like adding a row and setting field values as discussed
before. The one difference is that when you are updating an existing row, you
get the row change events fired after each field is changed immediately after
the column change events. So for each field that you modify you'll get four
events: ColumnChanging, ColumnChanged, RowChanging, and RowChanged.
One
final thing to note is that if you call AcceptChanges on a table, the row
change events fire for every row in the table with an Action value of Commit.
If you call RejectChanges on the table, the row change events will fire for
each row that has changes in it with an Action value of Rollback.
The
download code for this article has a simple console application that you can
use to experiment with to get used to when these events fire. You can play
around with modifying values in the event handlers to see what the effects are.
Using DataTable events gives you a lot of flexibility to be "in the loop" while
changes are occurring to data in memory, and allows you to validate or format
data values that are being put into the table, or trigger other processing as a
result of those changes. Using the events of the DataTable, you can design a
more event-driven program that can simplify your business logic for many
scenarios.
The
files accompanying this article are available for download.
Brian
Noyes is a
software architect with IDesign, Inc. (http://www.idesign.net),
a .NET-focused architecture and design consulting firm. Brian specializes in
designing and building data-driven distributed Windows and Web applications.
Brian writes for a variety of publications and is working on a book for
Addison-Wesley on building Windows Forms Data Applications with .NET 2.0.
Contact him at mailto:brian.noyes@idesign.net.