Sharing the goodness…
Beth Massi is a Senior Program Manager on the Visual Studio team at Microsoft and a community champion for business application developers. Learn more about Beth.
More videos »
In the last few posts on LINQ to SQL I've showed how to set up an object model using the O/R designer and how to handle a couple data binding scenarios with Comboboxes here and here. Last post on this topic we implemented a one-to-many data entry form and I showed how to work with stored procs as well as how to properly configure delete behaviors. In this post I want to explore how to easily add validation rules to our LINQ to SQL classes and how we can get these rules automatically displayed in the UI.
LINQ to SQL Classes -- A Closer Look
Classes that implement the IDataErrorInfo interface in conjunction with INotifyPropertyChanged are able to automatically notify UI objects like the ErrorProvider and the DataGridView to display validation errors. LINQ to SQL classes that are generated for you when you create your object models already implement the INotifyPropertyChanged interface and using partial methods we can easily add validation to our classes. For this example we'll expand the One-to-Many data entry form we built to include some business rules.
First let's open up the generated LINQ to SQL classes by opening the .Designer.vb file under the dbml file (if you don't see the designer file, just click the "show all files" button on the Solution Explorer tool strip first). If we take a look at our Order class we will see the following class definition:
<Table(Name:="dbo.Orders")> _ Partial Public Class [Order] Implements System.ComponentModel.INotifyPropertyChanging, _ System.ComponentModel.INotifyPropertyChanged
As you can see, this class is just a plain old CLR object (POCO) that implements interfaces that notify when properties are changing or changed on the class. That's it. It's the DataContext that does the heavy lifting, knowing what objects have changes, which ones were added and removed and how to persist these to the database. It does its simple mapping via the attributes on the class and it's properties. Here's the property for CustomerID on our Order class:
<Column(Storage:="_CustomerID", DbType:="Int NOT NULL", UpdateCheck:=UpdateCheck.Never)> _ Public Property CustomerID() As Integer Get Return Me._CustomerID End Get Set If ((Me._CustomerID = value) _ = false) Then If Me._Customer.HasLoadedOrAssignedValue Then Throw New System.Data.Linq.ForeignKeyReferenceAlreadyHasValueException End If Me.OnCustomerIDChanging(value) Me.SendPropertyChanging Me._CustomerID = value Me.SendPropertyChanged("CustomerID") Me.OnCustomerIDChanged End If End Set End Property
Partial Classes and Methods
In order to add validation and business rules we can add code to the OnCustomerIDChanging and OnCustomerIDChanged methods. But we don't add them here in this generated file, instead we add them into the Partial Class. This is possible because of a new feature in Visual Studio 2008 called Partial Methods. Partial Classes were introduced in Visual Studio 2005 as a way to extend generated classes with additional functionality by allowing you to split classes across physical files. You can add new methods or properties in order to extend a generated class easily by using the Partial Class keyword and then writing your own code. The compiler will "merge" these files into one class.
In Visual Studio 2008 we can go a step further using Partial methods. Instead of raising and handling private events, partial methods can be used instead as a better performing and cleaner alternative. They are declared by creating a private method with an empty body and decorating it with the Partial keyword. The method may then be "re-implemented" elsewhere within its containing class. If the method is implemented, then the compiler will redirect all calls to the partial method to the implementing method. If the method is not implemented in its containing class, then the compiler silently removes any calls to it from the program.
If we take a look at the generated LINQ to SQL Order class again you can see there is a region called "Extensibility Method Definitions" that contain Partial methods. There will be On...Changed and On...Changing partial methods here for each of the properties on the class.
#Region "Extensibility Method Definitions" Partial Private Sub OnLoaded() End Sub Partial Private Sub OnValidate(action As System.Data.Linq.ChangeAction) End Sub Partial Private Sub OnCreated() End Sub Partial Private Sub OnCustomerIDChanging(value As Integer) End Sub Partial Private Sub OnCustomerIDChanged() End Sub ...
Notice that the Changing and Changed methods are called at the appropriate times in each of the property setters. This allows us to write clean business rules on these properties in our partial class by defining the partial method. However if we don't write any code for these partial methods, the calls to them are removed from the IL. To access the partial class code open the model (dbml file) in the O/R designer, select a class, right-click on the class name and select "View Code". This will create a file named after your model where the partial class code that we write can reside.
So to write some business rules for our classes in this example we're going to place code in the On...Changing methods and then implement IDataErrorInfo so that we can have the UI automatically display the validation messages. To make it easier to implement IDataErrorInfo on all our LINQ to SQL classes I'm going to create a base class that we can inherit from called BaseBusiness.
Implementing IDataErrorInfo
IDataErrorInfo requires us to implement only two properties, one called Error and one default property Item that both return a string. Error is used to describe what is wrong with the entire object. For instance, if we are displaying this object in a row of a DataGridView this property indicates what error message appears on the row header. If you use DataSets, this corresponds to the DataRow.RowError property. The Item property is used to determine the error message for a specific property (or column) that is passed in. In order to collect these messages for each property on our object (i.e. column in our table) we can use a generic dictionary of strings and either add or remove messages from the dictionary depending on the validation rules. Here's an example implementation of IDataErrorInfo on our base class:
Imports System.ComponentModel ''' <summary> ''' Base class for our LINQ to SQL classes. ''' This class demonstrates one way to implement the IDataErrorInfo ''' interface so that the ErrorProvider and DataGridView can display ''' validation errors in the UI. ''' </summary> ''' <remarks></remarks> Public Class BaseBusiness Implements IDataErrorInfo 'This dictionary contains a list of our validation errors for each field Private validationErrors As New Dictionary(Of String, String) Protected Sub AddError(ByVal columnName As String, ByVal msg As String) If Not validationErrors.ContainsKey(columnName) Then validationErrors.Add(columnName, msg) End If End Sub Protected Sub RemoveError(ByVal columnName As String) If validationErrors.ContainsKey(columnName) Then validationErrors.Remove(columnName) End If End Sub Public Overridable ReadOnly Property HasErrors() As Boolean Get Return (validationErrors.Count > 0) End Get End Property Public ReadOnly Property [Error]() As String _ Implements System.ComponentModel.IDataErrorInfo.Error Get If validationErrors.Count > 0 Then Return String.Format("{0} data is invalid.", TypeName(Me)) Else Return Nothing End If End Get End Property Default Public ReadOnly Property Item(ByVal columnName As String) As String _ Implements System.ComponentModel.IDataErrorInfo.Item Get If validationErrors.ContainsKey(columnName) Then Return validationErrors(columnName).ToString Else Return Nothing End If End Get End Property End Class
Next we need to inherit from this class in all our LINQ to SQL Classes. Back in our partial class file we can inherit all our LINQ to SQL classes from BaseBusiness:
Partial Class Customer Inherits BaseBusiness End Class Partial Class Product Inherits BaseBusiness End Class Partial Class OrderDetail Inherits BaseBusiness End Class Partial Class Order Inherits BaseBusiness End Class
Writing the Business Rules
Now we need to write our business rules into these classes. Let's take Order as an example. There are a couple rules I want to implement here:
There are a couple places that we need to run these rules. One is when any of these fields change and the other is before the changes are submitted to the database. We need to handle both places because the user doesn't necessarily change all the properties and raise the On...Changing event. We need to handle the case when the user doesn't fill out a field and immediately attempts to save. This partial method is called OnValidate on our LINQ to SQL classes. This method is called automatically by the DataContext right before it attempts to submit the changes to the database when SubmitChanges is called.
Because you have to run the rules in both of these situations it's probably easier to create a private method that checks each rule and call that method from both Partial On...Changing and OnValidate methods. If the rule is broken then we just call AddError to add the error message to the dictionary, otherwise we call RemoveError to remove it from the dictionary. Here's the code for my Order Partial class:
Partial Class Order Inherits BaseBusiness Private Sub OnCustomerIDChanging(ByVal value As Integer) Me.CheckCustomerID(value) End Sub Private Sub OnOrderDateChanging(ByVal value As Date?) Me.CheckOrderDate(value) End Sub Private Sub OnShipDateChanging(ByVal value As Date?) Me.CheckShipDate(value) End Sub Private Sub OnValidate(ByVal action As System.Data.Linq.ChangeAction) Me.CheckCustomerID(Me.CustomerID) Me.CheckOrderDate(Me.OrderDate) Me.CheckShipDate(Me.OrderDate) If Me.HasErrors Then Throw New ValidationException(Me.Error) End If End Sub Private Sub CheckCustomerID(ByVal value As Integer) If value < 1 Then Me.AddError("CustomerID", "Customer cannot be empty.") Else Me.RemoveError("CustomerID") End If End Sub Private Sub CheckOrderDate(ByVal value As Date?) If value.HasValue Then If value > Me.ShipDate Then Me.AddError("OrderDate", "Order date cannot be after the ship date.") Else Me.RemoveError("OrderDate") End If End If End Sub Private Sub CheckShipDate(ByVal value As Date?) If value.HasValue Then If value < Me.OrderDate Then Me.AddError("ShipDate", "Ship date cannot be before the order date.") Else Me.RemoveError("ShipDate") End If End If End Sub Public Overrides ReadOnly Property HasErrors() As Boolean Get If Not MyBase.HasErrors Then 'Returns True if any order details are invalid For Each detail In Me.OrderDetails If detail.HasErrors Then Return True End If Next End If Return MyBase.HasErrors End Get End Property End Class
So here we are calling our validation methods from the On...Changing as well as the OnValidate partial methods. If you throw an exception from OnValidate then that will halt the SubmitChanges from going further. I created my own ValidationException class that we can use to check from our save code on the form. I also included any Order Detail errors in the HasErrors property of this Order class. This means that if any order details have errors then this Order also reports that HasErrors is True.
Displaying Validation Errors in the UI
Now that we have our business rules implemented we can hook up the UI to display these messages. Objects that are being displayed in a DataGridView will automatically pick up our validation messages but for simple controls like textboxes we need to hook up an ErrorProvider. Just drag the ErrorProvider from the toolbox onto your form and then specify the BindingSource as the DataSource property, for this example it's the OrderBindingSource. That's it. The ErrorProvider will look for any error messages on our objects through the IDataErrorInfo interface. This occurs when a property changes. For instance, if I run the form now and change the Order Date to be after the Ship Date, an error will be displayed as I tab off of the Order Date:
In order to display the messages after validation fails when we attempt to submit changes to the database, we need to write some code to refresh the display. In this scenario we also need to check the OrderBindingSource's position to make sure that the user is sitting on the invalid Order so the visuals are clear as to what needs fixing. This is why I created my own exception class called ValidationException and added a property to our BaseBusiness class called HasErrors, so that we could easily handle this case.
So back in our form we'll write the following Save code:
Private Sub OrderBindingNavigatorSaveItem_Click() _ Handles OrderBindingNavigatorSaveItem.Click Me.Validate() Me.OrderBindingSource.EndEdit() Me.OrderDetailsBindingSource.EndEdit() Try db.SubmitChanges() MsgBox("Your data was saved.") Catch ex As ValidationException Me.DisplayErrors() MsgBox("Please correct the errors on this form before saving.") Catch ex As Exception MsgBox(ex.ToString) End Try End Sub ''' <summary> ''' Displays any error information and navigates to the first error row ''' </summary> ''' <remarks></remarks> Private Sub DisplayErrors() Me.ErrorProvider1.UpdateBinding() Me.OrderDetailsDataGridView.Refresh()
If Me.OrderBindingSource.Position > -1 Then Dim currentOrder As Order = CType(Me.OrderBindingSource.Current, Order) If Not currentOrder.HasErrors Then 'The error is not in view so navigate to it For i = 0 To Me.OrderBindingSource.Count - 1 Dim order As Order = CType(Me.OrderBindingSource(i), Order) If order.HasErrors Then Me.OrderBindingSource.Position = i Exit Sub End If Next End If End If End Sub
To test this, add a new Order then navigate away from it. When you click Save the validation will fail and you will be positioned back on the order that failed.
I placed the code for this article (including the previous article code on this topic) into a Code Gallery project for you to play with.
Enjoy!
Great work.
There are couple of issues, when you add new record error info does not show up.
I had already written something similar to your BaseBusiness class except I was using a generic list rather than the dictionary object.
Public ColumnErrors As New List(Of ColumnError)
Public Class ColumnError
Public PropertyName As String
Public ErrorText As String
End Class
I have now changed my code now to use the dictionary object which I think is much more elegant - thanks for that!
I have used ADO.NET heavily until Linq to SQL arrived in VB9 and vastly prefer this approach for the data layer.
Beth, I enjoy reading you fantastic blogs - keep up the good work!
-Matt
Ok so we can validate, but how can we set defaults for new rows?
Hi Brian,
It depends on how you implement the list of business objects. In this example I'm just setting the results of the query directly to the DataSource so there is no default behavior. If you place the results of the query into your own subclass of the BindingList for instance, then you can override the AddNewCore method.
This is the same principle as normal object binding to your collections of business objects. Here's one of my favorite books on data binding: http://www.amazon.com/Data-Binding-Windows-Forms-2-0/dp/032126892X
HTH,
-B
Beth,
Thank you for the reply, and I apologize for the long delay.
Yes, I'm using VS 2008 RTM. And, yes, I'm looking at the designer file. Still losing hair!
Cheers,
Josh
After adding a primary key to my table, the code generated the INotify* implementation.
Thanks again,
Hi! I am sorry bad english. How validate work if i delete row
Orders.DeleteOnSubmit(dtl_item);
How recognize row state in LINQ? But delete row don't validate!
Hi! Validate and delete row. If use ADO.Net we write validate code ( that makr before save)
foreach (DataSet.SchetRow row in Schet.Rows)
{
switch (row.RowState)
case DataRowState.Added:
case DataRowState.Modified:
row.ClearErrors();
if (row.Isrsh_numNull())
h. e. we don't validate delete row! How true validate in LINQ?
Hi,
do you have an example to implement on an asp.net page?
thanks
Hi Beth! May be you don't understand me? You make test
1. Edit master- detail documnet
2 In detail part add some row and delete theirs
3 if detail class has validation
partial class detail
partial void OnamountChanging(decimal? value)
Checked_amount(value);
}
private void Checked_amount(decimal? value)
partial void OnValidate(ChangeAction action)
Checked_amount(this.amount);
if (HasErrod)
throw new Exception("No amount");
4. Save document
datacontext.SubmitChanges();
5 This method call OnValidate and delete row are present in process validate!!!
In Entity Framework row state present!
foreach (NaklModel.NaklDtl o in CurrentHdr.NaklDtl)
var state = context.ObjectStateManager.GetObjectStateEntry(o.EntityKey);
MessageBox.Show(
String.Format("{0}={1}",
state.EntityKey.EntityKeyValues[0] , state.State));
Hey Beth,
Good article. I have a question with regards to using this technique with WCF.
If I implement IDataErrorInfo on an entity in my datacontext and then use that entity in a WCF WebMethod, that same entity on the service consumer does not implement IDataErrorInfo. Is there a way to do this? I'd like to define my validation rules on the business entity generated by LINQ's ORM but through a web service consumed by a XBAP.
Thanks!
Hi Carlos,
The only way to do this is to use Type Sharing. (Add a reference to your business object entities and then add the Service Reference). But this solution only works if your client and service are both .NET. It's not SOA.
I don't have much experiance with XBAP but I think it would work.
As you can probably tell from my last couple posts I've been working with WPF in different data scenarios.
Hi! do you have an example to implement on an asp.net page?
thanks for answer bye :)