LINQ to SQL N-Tier Smart Client – Part 2 Building the Client

In my last post we built the service and data access layer for our LINQ to SQL N-Tier application. In this post we’ll walk through building a very simple Windows client form that works with our middle-tier.


Adding the Service Reference


Now that we have our middle-tier built it’s time to add the service reference to the client project. Sine we have both .NET on the server and the client I’m going to use type sharing so that we can reuse the business objects (LINQ to SQL classes) on both ends. If you recall we we already added a project reference on the client to the OMSDataLayer project that defines these types.


Once you add that project reference we can add the service reference by right-clicking on the client and selecting “Add Service Reference” which opens up the Visual Studio 2008 Add Service Reference dialog. Hit the Discover button and it will pick up the OMSService in our solution. Click on the “Advanced” button and you’ll notice some interesting settings here that I should mention.



Note here that the default is to “Reuse types in all referenced assemblies”. This means that since we added the project reference to our LINQ to SQL business objects first, when the service proxy is generated it will not create new classes on the client, instead it will reference our business object types directly. Although this can make versioning more of a challenge it drastically cuts down the amount of code we have to write to maintain our business rules because now they are shared. However note that rules we call from the client cannot access the database directly. Our application here does not have any rules like that but it’s something you may need to code for in your scenarios.


The other interesting settings I’ll mention are the Collection type and Dictionary collection type settings since we’re passing these types from our service. You can set these types to serialize differently if you need to. For instance, you can set the collection type to a BindingList if you are going to use all the collections from this service in typical data binding scenarios. Since this setting is for the entire service and we’re only going to need a BindingList for just our GetOrdersByCustomerID result, I’m opting to keep the default Array type instead.


Loading the Data


Now we’re ready to build our n-tier master-detail (one-to-many) form. Create a new form and then add a new data source (Menu, Data –> Add New Data Source) and select Object. Then expand the OMSDataLayer and choose the Order object and then do it again for Product.



Now we can build the master-detail form like I showed in this post (see the “Data Sources and Data Binding the Form” section) but this time against the objects in the shared assembly. The other main difference is that we don’t need the Customer object because we’re going to limit our data to just one customer.


Now we’re ready to create an instance of our service reference and load the Orders from the middle-tier. Since the list will deserialize as an array, I’m going to place them into a BindingList that the form will manage. This will give us automatic add/delete support to the collection and a better data binding experience. I’m also going to set up a couple lists to track deletes of Order and OrderDetails. In a real application typically you create your own subclass of the BindingList and have it track these things but I’m trying to keep this example simple. We’ll also load the products just like we did before but this time in our query we call the service instead.

Public Class NtierMasterDetailForm

Dim customerID As Integer = 1 ‘should come from a search form

Dim proxy As New OMSServiceReference.OMSServiceClient

Dim Orders As New BindingList(Of Order)
Dim DeletedOrders As New List(Of Order)
Dim DeletedDetails As New List(Of OrderDetail)

Private Sub Form1_Load() Handles MyBase.Load

‘Load the orders from our service
Dim orderList = proxy.GetOrdersByCustomerID(customerID)

For Each o In orderList
Me.Orders.Add(o)
Next

Me.OrderBindingSource.DataSource = Me.Orders

Dim emptyProduct As Product() = _
{New Product With {.Name = “<Select a product>”, .ProductID = 0}}

Me.ProductBindingSource.DataSource = (From Empty In emptyProduct).Union( _
From Product In proxy.GetProductList _
Order By Product.Name)
End Sub


Tracking Changes on the Objects


Now let’s see how we’re going to track all the changes made to the Orders and OrderDetails. First let’s take another look at our BaseBusiness class. This is the class that we created in this post when we implemented our validation. When we built the middle-tier I mentioned that we needed to add this property but it’s the client that needs to set it. Here’s a look at the modifications we need to make to the BaseBusiness object including adding the DataMember attribute to the new IsDirty property as well as on the ValidationErrors dictionary.

<DataContract()> _
Public Class BaseBusiness
Implements IDataErrorInfo

Private m_isDirty As Boolean
<DataMember()> _
Public Property IsDirty() As Boolean
Get
Return m_isDirty
End Get
Set(ByVal value As Boolean)
m_isDirty = value
End Set
End Property

‘This dictionary contains a list of our validation errors for each field
Private m_validationErrors As New Dictionary(Of String, String)

<DataMember()> _
Public Property ValidationErrors() As Dictionary(Of String, String)
Get
Return m_validationErrors
End Get
Set(ByVal value As Dictionary(Of String, String))
m_validationErrors = value
End Set
End Property
.
.
.


Since LINQ to SQL classes implement IPropertyNotifyChanged we can handle this event to set the IsDirty flag. The easiest way to set this flag is to tell the business objects themselves to do it. In order to hook up this event handler again when the objects are deserialized from the WCF service we can attribute a method with the OnDeserializedAttribute and add an event handler to the PropertyChanged event on all our business objects.

Partial Class Order
Inherits BaseBusiness

<OnDeserialized()> _
Private Sub OnDeserialized(ByVal context As StreamingContext)
AddHandler Me.PropertyChanged, AddressOf MyPropertyChanged
End Sub

Private Sub MyPropertyChanged(ByVal sender As Object,
ByVal e As System.ComponentModel.PropertyChangedEventArgs) _
Handles Me.PropertyChanged
If e.PropertyName <> “Customer” Then
Me.IsDirty = True
End If
End Sub
.
.
.
 


The trick in the handler is to set the IsDirty flag only if the entity reference (the parent reference) property is not being set because we want to only set this flag if the user is making changes, not when the collection reference is set by the system.


Tracking adds is really easy because when an object is added to the collection it will be sent to the middle-tier and we can use the primary keys to determine if the Order or OrderDetail is new. For instance, if the OrderID on the Order is equal to zero (OrderID = 0) then we know we have a new object in the collection.


Deletes are a bit trickier because when you delete an object from the collection it’s gone. If you are implementing a custom BindingList then you can just override the RemoveItem method but in our simple form we’re just going to add the Order or OrderDetail being deleted to our Deleted* lists when the delete buttons are clicked on the form.

Private Sub OrderNavigatorDeleteItem_Click() Handles BindingNavigatorDeleteItem.Click
‘Track deletes of orders
If Me.OrderBindingSource.Position > -1 Then
Dim order As Order = CType(Me.OrderBindingSource.Current, Order)
If order.OrderID > 0 Then
‘Greater than 0 indicates that the object came from the database.
‘If it’s = 0 then we know the object was added here then deleted
‘ and we don’t need to track that.
Me.DeletedOrders.Add(order)
End If
End If
End Sub

Private Sub DetailNavigatorDeleteItem_Click() Handles DetailNavigatorDeleteItem.Click
‘Track deletes of details
If Me.OrderDetailsBindingSource.Position > -1 Then
Dim detail As OrderDetail = CType(Me.OrderDetailsBindingSource.Current, OrderDetail)
If detail.OrderDetailID > 0 Then
Me.DeletedDetails.Add(detail)
End If
End If
End Sub


Validating and Saving our Changes


Before we send the changes to the service on the middle-tier we should validate the business objects here to save a round-trip. When we were working with the LINQ to SQL DataContext in connected mode the objects were validated when we called SubmitChanges(). This still happens in our middle-tier code but we need to validate here on the client as well so I added a public Validate method to the LINQ to SQL partial classes that just simply call into the OnValidate private methods we wrote previously. In the case of Order we’ll also validate any OrderDetails.

Partial Class Order
Inherits BaseBusiness
.
.
.

Public Sub Validate()
Me.OnValidate(System.Data.Linq.ChangeAction.None)

‘Validate the OrderDetails if there are any
For Each d In Me.OrderDetails
d.Validate()
Next
End Sub
.
.
.


Now we’re ready to write our save code. If everything validates here on the client we first then send the deletes to the middle-tier, and if all goes well there then we clear the lists where we were tracking those objects. Then we can send the added and updated rows into the middle-tier. The middle-tier will then perform the validation there and then update and insert the business objects, and return the added primary/foreign keys. If we had any additional middle-tier business rules then those would also run and we could add additional validation messages that would be sent back in the ValidationErrors collection on each object.


The last thing left to do is dump the collection coming back from the middle-tier with our added keys back into the BindingList on our form. We just need to suspend the data binding first then we can copy the array back into the BindingList collection. Here’s all the save code and supporting form methods.

Private Sub OrderBindingNavigatorSaveItem_Click() _
Handles OrderBindingNavigatorSaveItem.Click

Me.Save()
End Sub

”’ <summary>
”’ Saves all changes to the middle-tier
”’ </summary>
”’ <remarks></remarks>
Private Sub Save()
‘Push any pending edits on the BindingSources to the BindingList
Me.Validate()
Me.OrderBindingSource.EndEdit()
Me.OrderDetailsBindingSource.EndEdit()

Dim saved = True

‘Only save changes if there are some and they are valid
If Me.HasChanges AndAlso Me.ValidateOrders() Then

Dim saveOrders = Me.Orders.ToArray

Try
If Me.DeletedDetails.Count > 0 OrElse Me.DeletedOrders.Count > 0 Then
‘Delete any orders/details
If proxy.DeleteOrders(Me.DeletedOrders.ToArray, _
Me.DeletedDetails.ToArray) Then

Me.DeletedDetails.Clear()
Me.DeletedOrders.Clear()
Else
saved = False
End If
End If

If saved Then
If saveOrders.Length > 0 Then
‘Update/insert orders/details
saved = proxy.SaveOrders(saveOrders)
End If
End If

Catch ex As Exception
MsgBox(ex.ToString)
End Try

‘merges added keys and any validation errors from the middle-tier
Me.MergeOrdersList(saveOrders)
End If

If Me.HasErrors Then
‘Display any errors if there are any (same technique as before)
Me.DisplayErrors()
MsgBox(“Please correct the errors on this form.”)
Else
If saved Then
MsgBox(“Your data was saved.”)
Else
MsgBox(“Your data was not saved.”)
End If
End If
End Sub

”’ <summary>
”’ Returns True if there are any validation errors on the business objects.
”’ </summary>
”’ <returns></returns>
”’ <remarks></remarks>
Private Function HasErrors() As Boolean
For Each o In Me.Orders
If o.HasErrors Then Return True
Next
Return False
End Function

”’ <summary>
”’ Validates all the orders (order details are validated in the Order.Validate)
”’ </summary>
”’ <remarks></remarks>
Private Function ValidateOrders() As Boolean
Try
For Each o In Me.Orders
o.Validate()
Next
Catch ex As ValidationException
Return False
End Try
Return True
End Function

”’ <summary>
”’ Returns True if there are any changes to any of the orders/details.
”’ </summary>
”’ <returns></returns>
”’ <remarks></remarks>
Private Function HasChanges() As Boolean
If Me.DeletedDetails.Count > 0 OrElse Me.DeletedOrders.Count > 0 Then
Return True
End If
For Each o In Me.Orders
If o.IsDirty Then Return True
For Each d In o.OrderDetails
If d.IsDirty Then Return True
Next
Next
Return False
End Function

”’ <summary>
”’ Copies from array to the BindingList while suspending data binding
”’ </summary>
”’ <param name=”changes”></param>
”’ <remarks></remarks>
Private Sub MergeOrdersList(ByVal changes() As Order)
Dim pos = Me.OrderBindingSource.Position
Me.OrderBindingSource.SuspendBinding()
Me.OrderBindingSource.RaiseListChangedEvents = False

Me.Orders.Clear()
For Each o In changes
Me.Orders.Add(o)
Next

Me.OrderBindingSource.ResumeBinding()
Me.OrderBindingSource.RaiseListChangedEvents = True
Me.OrderBindingSource.Position = pos
End Sub


And that’s basically it. As you can see even in it’s simplest implementation (that I could think of) writing n-tier applications with LINQ to SQL takes some work, especially as the relations between our object collections increase. LINQ to SQL is really just used as the data access technology in the middle-tier, everything on top of that is up to us to implement as we see fit for our particular scenarios.


You can download the sample application on CodeGallery here.


Enjoy!