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.

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.

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.

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.

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.

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.

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.


Beth Massi

Follow Beth   


Comments are closed.