You’re building a Smart Client application. You want this Smart Client to be smart enough to provide validation feedback to the user as they enter information. However, in order to make the application scale to thousands of users you can’t open connections directly to the database from the client because you’re server can’t handle that many connections. You probably don’t want the data access information to be available on the client anyway so you turn to Web Services to help abstract the intimate knowledge about your database. This way the client only has Web Service address and authentication information. Since web services can be consumed by many different types of clients you don’t want to trust that all the validation rules have been enforced. So, what do you do?
The likely solution is to simply write the validation logic and share it on the client and within the web service. But wait, that’s type sharing… that’s bad… Well, yes and no. In the old days of COM/DCOM and even with Remoting you really were sharing the same actual object. The client would instantiate a component and it would actually run on a remote server. In addition to scalability issues, this also had a maintenance issue. If you ever updated the component on the server, even if it didn’t involve changing the public interface, all the clients had to be updated as well. To make this more complicated, since clients are running statefull operations on shared servers you usually had to completely shut down the entire system to make a simple update. Web Services solves these problems as the object being used and what travels across the wire are really individual things. The serialization format of a particular object has no real direct correlation to the actual object. Yes, certain objects serialize certain types of formats, but that’s not a hard and fast rule. I can talk in English, French, German, Hungarian, but I’d still be saying the same thing. Likewise, 3 different people can all say the same thing in English, and it would have the same meaning. Once you separate the wire protocol from the functionality you can change the internal implementation all you want, and as long as you don’t change the public interface and your clients can continue to consume the service.
Now I know that many would argue that changing functionality within a service is breaking the contract. And to some extent you’re correct. If you’re exposing a public service, and don’t own all the clients then you do have an obligation to maintain consistency. However, if you do own the clients and the services are only consumed as part of your application then this becomes a very flexible model.
Take the following scenario:
You roll out your application over the weekend. Thousands of users fire up the app first thing Monday morning. They all get the updated version with ClickOnce deployment. Life is good. Later that day you notice that you forgot to put a validation rule in that all orders can’t be shipped for 7 days after the order date. You quickly add the validation rule to enforce DueDate must be Today + 7 Days and you add a default value that DueDate is Today + 7 days, but how do you deploy this? You could announce over the PA system that everyone must exit the sales system for 5 minutes which effectively shuts the company down and directly affects the profits of the day. That probably wouldn’t reflect well on your next review.
Or, you could update the validation logic on the server without touching the client. Since the public interface didn’t change, the clients continue to function. Since ASP.net Web Services use something called Shadow Copied assemblies, you aren’t blocked from updating an assembly in the web server. Once the file is finished being copied, the webserver detects that one of the assemblies that’s using has changed and reloads that assembly into memory. The next request will immediately get the new functionality. At this point the client doesn’t have the validation logic, however, when the sales rep attempts to save an order the additional validation logic will kick in and send back the error to the client. If the developer updated the client app with the new assembly then the clients will get updated the next time they restart their app.
You could use the ClickOnce background APIs to constantly check for updates, and that’s a good thing but you don’t want to be checking or updates ever 10 seconds. That sort of defeats the purpose here. In the above example, you were able to immediately affect a change without interrupting any of the sales reps which means no downtime, and you’ve got a great review again.
So, how do you implement all this? The basic concepts, including some of the proxy generation issues of Web Services equally apply to custom objects, but for this article I’ll focus on how to leverage Typed DataSets to enable validation on the client but enforce it on the server.
One of the major features of Visual Studio 2005 is the new typed data access components called TableAdapters. These can be used within your Data Access Layer to handle all the CRUD operations to the database. TableAdapters are effectively strongly typed DataAdaters. These bring parity with the Typed DataSet experience in Visual Studio 2002. One of the VS 2005 enhancements of Typed DataSets is the use of partial classes. In 2002/03 it was very difficult to add custom logic to the Typed DataSet. Since Visual Studio uses something called Single File Generators to generate the Typed Datasets, any code the developer added to the Typed DataSet would be lost the next time the generator ran. Developers would sometimes inherit from the Typed DataSet but that had lots of other complications as well. It further complicated things as you now had two types; the one generated by Visual Studio and the one that had your validation code.
Visual Studio 2005 Typed DataSets leverage a feature known as partial classes. Using partial classes developers can now directly enhance the functionality of the generated Typed DataSet without the possibility of being overwritten by the Single File Generator. This means we can now easily add our validation code directly to the Typed DataSet and enforce it on the server. However, because Visual studio generates the Typed DataSet in the same file and project as the TableAdapters we don’t have an easy way to leverage the Typed DataSet on the client. We could add a reference to the assembly that contains the Typed DataSet and TableAdapters on the client, but now the client has the intimate knowledge of the server that we’re trying to avoid?
Great, so now what? Well, it turns out it’s not all that difficult to separate these. It’s not very discoverable, but it’s not that difficult. What we’re going to do is separate the Typed DataSet from the DataLayer into its own DataEntities assembly. We’ll create a Web Service to return the Typed DataSet and use the DataLayer assembly behind the web service. The client application will share the reference to the DataEntities assembly on the client but it won’t have the DataLayer so we’ll keep the intimates of the database away from the client. Since Web Services serializes DataSets as XML we can leverage the same type on both sides of the wire, but we’re not actually sharing the same instance so we get a lot of flexibility in how we update the application.
The rest of this article focuses on a walkthrough for how to accomplish this. I’ll highlight some of the best practices for this scenario to minimize complexities for working around the default behavior of the tool.
To get things started we’ll create a solution with the projects to represent the logical and physical tiers.
Creating the solution
1. Using Visual Studio 2005 create a new solution containing 4 projects. It doesn’t matter whether you’re using VB or C#.
Project Type & Description
A Windows Forms project
A Class Library that will contain our Typed DataSet with validation logic
A Class Library that will load and save the Typed DataSet in the DataEntities dll
A WebService that will simply wrap calls to the DataLayer
2. Delete Class1 in the Class Libraries
3. Delete the Service.asmx and Service.vb/cs file in the App_Code directory
4. You should now have a solution that looks something like the following:
Adding a Typed DataSet/TableAdapters to the DataLayer
1. With the DataLayer selectged, open the Data Sources Window. (You can do this using the Data Menu and select Show Data Sources).
2. Add a new Data Source and choose Database
3. Add a connection to your database. In this walkthrough we’ll use the Northwind database
4. Using the treeview, choose your tables. We’ll simply select the Orders table for this walkthrough. You could use sprocs or views, but to keep this walkthrough focused, we’ll just select the orders table and press finish.
5. You should now have a Typed DataSet and TableAdapter for the Northwind.Orders table
Moving the DataSet to the DataEntities project
We’ll now cut and paste the Typed DataSet definition from the DataLayer to the DataEntities project to isolate the intimate knowledge of the database from the data entities.
Fixing up the references
If you build the project you’ll now see a bunch of errors in the task list. The TableAdapters reference a type that is no longer “visible”.
C#: Open the NorthwindDataSet.Designer.cs file and add an additional Using statement for the DataEntities project: using DataEntities;
We now have data access and data entities isolated and the have the proper references. Notice that while the DataLayer does have a reference to the DataEntites the opposite is not true. This means the DataEntiteis can be used without any database intimates. The next problem is how to get the DataEntites from the DataLayer to the client. This is where Web Services come in. Using Web Services we can abstract the client form the server logic. Note that this same process will work for the upcoming Windows Communication Framework in WinFX.
Exposing the DataEntities via Web Services
In order to expose the DataEntities via Web Service we’ll create some thin wrappers to delegate any calls to the DataLayer
Private _ordersTableAdapter As DataLayer.NorthwindDataSetTableAdapters.OrdersTableAdapter
Public Sub New()
_ordersTableAdapter = New DataLayer.NorthwindDataSetTableAdapters.OrdersTableAdapter()
Public Function GetOrders() As DataEntities.NorthwindDataSet.OrdersDataTable
Public Function SaveOrders(ByVal orders As DataEntities.NorthwindDataSet.OrdersDataTable) As DataEntities.NorthwindDataSet.OrdersDataTable
If orders.HasErrors Then
Consuming the DataEntities in your WinForms client
We’ll now add the ability to load and save a form full of customers. In the client project we’ll add a Web reference to our BizServices to get the load and save functionality and we’ll add an additional reference to the DataEntities dll.to leverage common validation logic on the client and the server. Note that we won’t have any reference to the DataLayer dll so the client will be clean of data access knowledge.
Merging the proxy and original types
Although we’ll use the Web Service to get and save orders, we’re not going to use this TypedDataSet to represent our data within our client app. Instead we’re going to use the DataEntities dataset
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Notice that DataTables now support merging directly. In 1.x, you had to merge the entire DataSet. Also note that we were able to return an individual Typed DataTable rather then the entire DataSet. This only works for Typed DataTables, You can’t return a single UnTyped DataTable
Private Sub OrdersBindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles OrdersBindingNavigatorSaveItem.Click
Dim orderServiceOrderTable As New Client.OrdersService.NorthwindDataSet.OrdersDataTable()
orderServiceOrderTable = My.WebServices.Orders.SaveOrders(orderServiceOrderTable)
If orderServiceOrderTable.HasErrors Then
MessageBox.Show(Me, "Errors on attempt to save")
Adding Validation Code
Now that we’ve got the client project setup and deployed, let’s add some validation logic. In order to demonstrate the lack of tight coupling, we’ll add the validation logic to the DataEntities project, but we won’t actually deploy this to the client. So, when testing this, be sure not to republish the client.
Private Sub OrdersDataTable_ColumnChanged(ByVal sender As Object, ByVal e As System.Data.DataColumnChangeEventArgs) Handles Me.ColumnChanged
' We use the Changed event as we try to avoid deleting information a user enters
' Rather we show them what they typed, but provide information indicating the specific error
' This helps users understand if they mistyped, or the information is just not permitted
' Use strongly typed names of the columns to benefit from compile time verification
' If the user changed the OrderDate or ShippedDate columns, verify the dates
If e.Column.ColumnName = Me.OrderDateColumn.ColumnName Or _
e.Column.ColumnName = Me.ShippedDateColumn.ColumnName Then
Private Sub ValidateDates(ByVal row As OrdersRow)
' Check for specific column errors and set or clear the errors
' Using this model, DataSets will surface the errors using the IDataError interface
' DataBinding in the DataGridView and ErrorProvider will pick up these errors
' and surface them to the user
' You may want to use strongly typed Resources for the error strings.
If row.OrderDate > row.ShippedDate Then
row.SetColumnError(Me.OrderDateColumn, "Can't ship before it's been ordered")
row.SetColumnError(Me.ShippedDateColumn, "Can't ship before it's been ordered")
Updating the client
To update the client, simply republish the Client project using the ClickOnce deployment wizard and restart your client app.
With the above scenarios you were able to make changes to the DataEntities without breaking the client applications. You’re users maintained their productivity, yet you were able to quickly add new validation rules. The clients could then be updated to complete the update, but you didn’t break you’re users workflow in the process. With the basic tooling support in Visual Studio and these tricks you can achieve a truly factored ‘N scale application that leverages code where you want, scales to thousands of users, can be updated mid day without kicking your users off the system and delivers on the promise of Smart Clients for productive users.
Post Whidbey, we are working to enable this scenario without all the work arounds noted above, but for now…