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
"SELECT * FROM Customers",m_ds,
// Wire up the events
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 =
// Populate the fields that are non-null
newrow.CompanyName = "FooBros";
newrow.CustomerID = "FOOBR";
// Add it to the table
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,
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:[email protected].