Welcome to MSDN Blogs Sign in | Join | Help

How to retrieve stored procedure return values from TableAdapter

If you’ve been wondering why you are not able to access stored procedure return values from TableAdapter, here’s the solution for you.

I will use tbTasks table and spInsertTask stored procedure to demonstrate the solution.  You can see definitions for both below.

CREATE TABLE dbo.tbTasks

(

       intID INT NOT NULL IDENTITY(1,1),

       strName VARCHAR(100) NOT NULL,

       intPriority INT NOT NULL,

       dtDueDate DATETIME NOT NULL

)

----------------------------------------

CREATE PROCEDURE dbo.spInsertTask

       (

              @strName VARCHAR(200),

              @intPriority INT,

              @dtDueDate DATETIME

       )

AS

 

INSERT INTO tbTasks (strName, intPriority, dtDueDate)

VALUES (@strName, @intPriority, @dtDueDate)

 

RETURN SCOPE_IDENTITY()

GO

 

Notice that tbTasks has an identity column named intID.  Also, stored procedure spInsertTask returns the new identity column value using SCOPE_IDENTITY().  Knowing this new identity value is extremely useful on the client side.

Create a new Typed Dataset called TasksDataset and add tbTasks.  Also, add a new query to tbTasksTableAdapter using spInsertTask stored procedure.  When adding a new query, choose ‘A single value’ option.

At this point, you probably would expect that following code would assign the new identity value returned by spInsertTask stored procedure to returnValue variable.

[ VB ]

Dim taTasks As New TasksDatasetTableAdapters.tbTasksTableAdapter

Dim TaskName As String

Dim TaskPriority As Integer

Dim TaskDueDate As Date

Dim returnValue As Integer

 

TaskName = "Test"

TaskPriority = 1

TaskDueDate = Now()

 

returnValue = taTasks.InsertTask(TaskName, TaskPriority, TaskDueDate)

 

[C#]

TasksDatasetTableAdapters.tbTasksTableAdapter taCustomers = new WindowsApplication1.TasksDatasetTableAdapters.tbTasksTableAdapter();

String taskName;

int taskPriority;

DateTime taskDueDate;

int returnValue;

 

taskName = "Test";

taskPriority = 1;

taskDueDate = System.DateTime.Now;

 

returnValue = taCustomers.InsertTask(taskName, taskPriority, taskDueDate);

 

However, running above code results in System.InvalidOperationException during run-time for VB and “Cannot implicitly convert type 'int?' to 'int'.” compile error for C#.  If you look at what actually gets returned by tbTasksTableAdapter.InsertTask() function, you will understand why above code does not work.  You can find the function from the generated Typed Dataset code, TasksDataset.Designer.vb / TasksDataset.Designer.cs in this case.

[ VB ]

Public Overridable Overloads Function InsertTask(...) As System.Nullable(Of Integer)

    ...

    ...

    Dim returnValue As Object

    Try

        returnValue = command.ExecuteScalar

    Finally

        ...

    End Try

    If ((returnValue Is Nothing) _

                OrElse (returnValue.GetType Is GetType(System.DBNull))) Then

        Return New System.Nullable(Of Integer)

    Else

        Return New System.Nullable(Of Integer)(CType(returnValue, Integer))

    End If

End Function

* C# version omitted since there’s no significant difference.

As you can see from above, what gets returned from InsertTask function is actually the return value of System.Data.SqlClient.SqlCommand.ExecuteScalar() which is the first column of the first row in the result set, or a null reference if the result set is empty, not the return value of the stored procedure.  In this case, InsertTask returns null since the stored procedure does not return any result set.

If you choose ‘No value’ option, System.Data.SqlClient.SqlCommand.ExecuteNonQuery() is used instead.  And the return value of ExecuteNonQuery() is the number of rows affected.  Again, this is not the stored procedure return value.

So, how do you retrieve the stored procedure return value?  Although it’s not immediately obvious, there’s an easy way to access it.  Let’s look at the definition of the command object for the stored procedure.  You can see it from tbTasksTableAdapter.InitCommandCollection() in TasksDataset.Designer.vb / TasksDataset.Designer.cs file.

[ VB]

Private Sub InitCommandCollection()

    Me._commandCollection = New System.Data.SqlClient.SqlCommand(1) {}

    Me._commandCollection(0) = New System.Data.SqlClient.SqlCommand

    Me._commandCollection(0).Connection = Me.Connection

    Me._commandCollection(0).CommandText = "SELECT intID, strName, intPriority, dtDueDate FROM dbo.tbTasks"

    Me._commandCollection(0).CommandType = System.Data.CommandType.Text

    Me._commandCollection(1) = New System.Data.SqlClient.SqlCommand

    Me._commandCollection(1).Connection = Me.Connection

    Me._commandCollection(1).CommandText = "dbo.spInsertTask"

    Me._commandCollection(1).CommandType = System.Data.CommandType.StoredProcedure

    Me._commandCollection(1).Parameters.Add(New System.Data.SqlClient.SqlParameter("@RETURN_VALUE", System.Data.SqlDbType.Int, 4, System.Data.ParameterDirection.ReturnValue, 10, 0, Nothing, System.Data.DataRowVersion.Current, false, Nothing, "", "", ""))

    Me._commandCollection(1).Parameters.Add(New System.Data.SqlClient.SqlParameter("@strName", System.Data.SqlDbType.VarChar, 200, System.Data.ParameterDirection.Input, 0, 0, Nothing, System.Data.DataRowVersion.Current, false, Nothing, "", "", ""))

    Me._commandCollection(1).Parameters.Add(New System.Data.SqlClient.SqlParameter("@intPriority", System.Data.SqlDbType.Int, 4, System.Data.ParameterDirection.Input, 10, 0, Nothing, System.Data.DataRowVersion.Current, false, Nothing, "", "", ""))

    Me._commandCollection(1).Parameters.Add(New System.Data.SqlClient.SqlParameter("@dtDueDate", System.Data.SqlDbType.DateTime, 8, System.Data.ParameterDirection.Input, 23, 3, Nothing, System.Data.DataRowVersion.Current, false, Nothing, "", "", ""))

End Sub

* C# version omitted since there’s no significant difference.

You can see from above that parameters collection does actually include @RETURN_VALUE parameter.  It’s the first parameter in the collection.  When the stored procedure is executed, return value from the stored procedure is added to this item in the collection.  So, all we need to do is to retrieve this value after executing InsertTask() function.  To do that, I will add some code to the partial class defined in TasksDataset.vb / TasksDataset.cs.

[ VB ]

Namespace TasksDatasetTableAdapters

    Partial Public Class tbTasksTableAdapter

        Public Function GetReturnValue(ByVal commandIndex As Integer) As Object

            Return Me.CommandCollection(commandIndex).Parameters(0).Value

        End Function

    End Class

End Namespace

 

[ C# ]

namespace WindowsApplication1.TasksDatasetTableAdapters {

    public partial class tbTasksTableAdapter

    {

        public object GetReturnValue(int commandIndex)

        {

            return this.CommandCollection[commandIndex].Parameters[0].Value;

        }

    }

}

 

Since Dataset Designer does not generate partial class structure for TableAdapters, you will have to add above code yourself to partial class file.  The commandIndex parameter is the index of the command object in _commandCollection to retrieve return value from.  You can get that information by looking at tbTasksTableAdapter.InitCommandCollection(). Now, let’s modify the code that was not running to use this new function.

[ VB ]

Dim taTasks As New TasksDatasetTableAdapters.tbTasksTableAdapter

Dim TaskName As String

Dim TaskPriority As Integer

Dim TaskDueDate As Date

Dim returnValue As Integer

 

TaskName = "Test"

TaskPriority = 1

TaskDueDate = Now()

 

taTasks.InsertTask(TaskName, TaskPriority, TaskDueDate)

returnValue = taTasks.GetReturnValue(1)

 

[C#]

TasksDatasetTableAdapters.tbTasksTableAdapter taCustomers = new WindowsApplication1.TasksDatasetTableAdapters.tbTasksTableAdapter();

String taskName;

int taskPriority;

DateTime taskDueDate;

int returnValue;

 

taskName = "Test";

taskPriority = 1;

taskDueDate = System.DateTime.Now;

 

taCustomers.InsertTask(taskName, taskPriority, taskDueDate);

returnValue = (int)taCustomers.GetReturnValue(1);

 

We pass in 1 as a parameter value to GetReturnValue() since our stored procedure is located at index 1 in _commandCollection.  Above code will correctly retrieve return value from the stored procedure which is the new identity value of intID column.  If you have more than one stored procedures that return something, you can retrieve those return values by calling GetReturnValue() with correct index.

Typed Dataset simplifies data access layer development significantly by generating necessary code for you based on the information you provide via Dataset Designer.  Although generated code covers large number of scenarios, I suggest that you take a close look at generated code and find out how you can extend the functionality of default Typed Dataset.  And definitely let us know how we can improve it to make Typed Dataset more powerful and flexible.

Also, don’t forget to let me know if you have better ways to retrieve return values from stored procedures.  What I suggest here is just one solution and I am sure that you have other solutions that might be more elegant than this.

Young Joo

Posted by youngjoo | 18 Comments
Filed under:

Q&A About SQL Server Everywhere

Steve posted some answers to many of the questions about SQL Server Everywhere...

http://blogs.msdn.com/stevelasker/

 

SQL Server Everywhere - SQL Mobile Unleashed

Today Paul Flessner announced that SQL Mobile will be released as SQL Server Everywhere Edition.   http://www.microsoft.com/sql/letter.mspx Details are yet to be announced.  But to get a feel for what the new product will do, check out this walkthrough: The little database that could.

Steve

Splitting Typed DataSets from TableAdapters - Sharing Validation Code across the tiers

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? 

  • You could put all the validation rules in the database, but that means your database has that much more processing to do, and your Smart Client won’t be so smart.  It will still have to make round trips to the database each time a user tabs off a control. 
  • You could use the XSD schema validation extensions, but they aren’t all that extensible and they don’t deal with dynamic values.
  • You could write the validation code twice.  Once for the server, then again for the client.  But that wouldn’t be all that maintainable. 

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 Name

Project Type & Description

Client

A Windows Forms project

DataEntities

A Class Library that will contain our Typed DataSet with validation logic

DataLayer

A Class Library that will load and save the Typed DataSet in the DataEntities dll

BizServices

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.

  1. Show the hidden files by pressing “Show All Files” button in the top of the solution explorer
  2. Open the NorthwindDataSet.Designer.vb/cs file
    In this file you’ll see two major sections.  The definition of the Typed DataSet and a namespace for all the TableAdapters associated with the Typed DataSet
    It’s easiest to see the source if you collapse the regions for the Typed DataSet and the TableAdapter namespace
  1. Copy the source code from the first line through the definition of the Typed DataSet
  2. In the DataEntities project add a new Class File and name it the same as your DataSet.  You’ll want to add the .Designer extension to maintain separation between the generated code and the validation code we’ll add later on.  In this case we’ll create NorthwindDataSet.Designer.vb
  3. In this file paste over the entire contents with the source you copied from the DataLayer.dll
  4. Back in the DataLayer project, delete the DataSet definition.  Be sure to leave the headers, Imports and if using VB, the Option Strict code

 

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”. 

  1. In the DataLayer project, add a reference to the DataEntities project.  This will solve the issue of finding the types. 
  2. You’ll now need to add a project level imports to resolve any namespace differences between the entities and DataLayer. 
    VB: Select the DataLayer project and double click the My Project node to open the Project Properties.  Select the References tab and using the Imported Namespaces check the DataEntities namespace.  The errors should now disappear from the task list.

C#: Open the NorthwindDataSet.Designer.cs file and add an additional Using statement for the DataEntities project:
using DataEntities;

  1. Once you build the solution you should no longer see any build errors.

 

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

  1. In the BizServices web service project select add new Web Service
  2. Name the Web Service Orders.  Visual Studio will create two files.  One for IIS to expose as an address, Orders.asmx.  And another for the code, Orders.vb/cs
  3. In the BizServices project add references to the DataEntities and the DataLayer projects.
  4. In the Orders.vb/cs file add the following code to expose the Load and Save methods:

    Private _ordersTableAdapter As DataLayer.NorthwindDataSetTableAdapters.OrdersTableAdapter

    Public Sub New()

        _ordersTableAdapter = New DataLayer.NorthwindDataSetTableAdapters.OrdersTableAdapter()

    End Sub

    <WebMethod()> _

    Public Function GetOrders() As DataEntities.NorthwindDataSet.OrdersDataTable

        Return _ordersTableAdapter.GetData()

    End Function

    <WebMethod()> _

    Public Function SaveOrders(ByVal orders As DataEntities.NorthwindDataSet.OrdersDataTable) As DataEntities.NorthwindDataSet.OrdersDataTable

        If orders.HasErrors Then

            Return orders

        Else

            _ordersTableAdapter.Update(orders)

            Return orders

        End If

    End Function

  1. We now have the ability to return and save Orders to the database.  The sample above does do some validation testing within the web service which you’d likely want to place in a separate assembly.  You could place it in the DataLayer as well  The point is you want to minimize the actual code in the Web Services project so you’ll have more flexibility for where to use this code.  For instance, when WCF ships you’ll likely want to leverage some of the new transports.  By keeping the code in the web service to an absolution minimum you’ll ease your migration process..  
    Another thing worth notice is while this sample focuses on a simple 1:1 mapping of the database to the returned DataSet, that isn’t the limitation of DataSets.  The TableAdapter wizards are optimized for relatively simple mappings, but using ADO.net commands, Stored Procedures and/or Views, you can do quite a bit of additional logic to differentiate the schemas of your DataSet from the underlying database.  Using the merge capabilities of DataSet, you can have a single DataSet or even a single DataTable represent data from two or more database systems.  But, that’s a discussion for another day.

 

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.

  1. Using Solution Explorer select Add Web Reference in the Client project.  You could use the Data Sources Window, but we’ll want to rename the service and this feature isn’t available in the Data Sources Window wizard.
  2. In the Web Reference wizard browse Web services in this solution.  You should now see your Orders service. 
  3. Since LocalHost is fairly meaningless, select the Orders Service and rename it OrdersService.  Press finish to continue
  4. If you notice the Data Sources Window you’ll see the dataset returned by the web service is now available.  What’s important to note is while this dataset is the same “shape” as the data set returned by the Web Service, it’s not the same Type as the DataSet in the DataEntities project.  The Web Reference Dialog created a proxy type for the dataset.  If you show the hidden files you can navigate through the OrdersService, Reference.map to find the refernce.vb file.  This contains the code necessary to call the OrdersService, but it also contains a type definition for the NorthwindDataSet.  While it is declared as Partial, we’re not going to leverage this feature. 

    Due to time constraints in the Visual Studio 2005 schedule we were unable to complete a feature we refer to as Proxy Type Sharing.  If you consume multiple web services from the same project that return the same type, Visual Studio will create multiple proxy types on the client.  This means you could actually wind up with several TypedDataSets on the client that all share the same shape.  However, even if we fixed Proxy Type Sharing, we still wouldn’t have exactly what we need in this scenario.  In this case, we already have the type on the client, so we don’t need a proxy.  In a few steps, we’ll see how we consolidate these.

 

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

  1. With the Client project active, use the Data Sources Window to add an Object Data Source.  Add a reference to the DataEntities project and select the NorthwindDataSet as your Object DataSource. 
    Visual studio differentiates Object Data Sources from Database DataSources by providing a different design experience.  When the developer chooses Database datasources, Visual Studio creates Typed DataSets and TableAdapters.  When you drag & drop Database DataSources from the Data Sources Window Visual Studio will add additional load and save code to your form.  For Object Data Sources, Visual Studio doesn’t make any assumptions about how you load and save your objects.  Typed DataSets are simply very specialized data object containers that have all the necessary plumbing for serializing and databinding.
  2. With Form1 open, select the DataEntities.Northwind.Orders object in the Data Sources Window and drag it to the form.  Be sure you’re getting the right DataSet, it can be confusing as they’re both named similarly. 
    1. You can remove the Web Service DataSet proxy from the Data Sources Window with a little work around.  If you show hidden files in the Client project and navigate through OrdersService -> Reference.map you’ll see a NorthwindDataSet.datasource file.  This file is what Visual Studio uses to show object in the Data Sources Window.  If you delete this file the entry will be removed from the Data Sources Window but the Web Service will still exist. If you ever update the web reference the .datasource file will be recreated.
  3. Double click the form to get the form.load event and add the following code to merge the DataSet returned by the web service with the DataEntities DataSet.
    1. We could do another work around and change the code generated by the web service proxy generator, however this is much more complicated to maintain.  Since DataSets and DataTables support merge capabilities, we can avoid changing the generated code.  For plain old objects, (POOs) that don’t support merge capabilities, you’ll likely want to edit the proxy generated code.

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        Me.NorthwindDataSet.Orders.Merge(My.WebServices.Orders.GetOrders())

    End Sub

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

  1. Activate the form designer again and enable the Save button that was created in the BindingNavigatgor
  2. Double click the Save button and add the following code to commit any changes made by the user and send the changes to the server.

    Private Sub OrdersBindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles OrdersBindingNavigatorSaveItem.Click

        Me.Validate()

        Me.OrdersBindingSource.EndEdit()

        Dim orderServiceOrderTable As New Client.OrdersService.NorthwindDataSet.OrdersDataTable()

        orderServiceOrderTable.Merge(Me.NorthwindDataSet.Orders.GetChanges())

        orderServiceOrderTable = My.WebServices.Orders.SaveOrders(orderServiceOrderTable)

        If orderServiceOrderTable.HasErrors Then

            MessageBox.Show(Me, "Errors on attempt to save")

        End If

        Me.NorthwindDataSet.Orders.Merge(orderServiceOrderTable)

    End Sub

  1. Hit F5 to run you’re app
  2. If you look in the bin directory of the Client app you’ll notice we only have the DataEntities.dll and Client.exe files.  The DataLayer which contains all the intimate knowledge of the Databas are left out of the picture. 
  3. Using ClickOnce, publish the Client project and install it on your machine.  Be sure to use the default behavior so you get the Start menu item

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.

  1. In the DataEntities project add a new class file.  Name it NorthwindDataSet.vb/cs.  Note that we’re using the same standard as forms and the DataSet designer to isolate designer generated code from your code.
  2. In the file add the following code:

        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

                ValidateDates(e.Row)

            End If

        End Sub

        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")

            Else

                row.SetColumnError(Me.OrderDateColumn, "")

                row.SetColumnError(Me.ShippedDateColumn, "")

            End If

        End Sub

  1. Rebuild the entire solution to make sure the updated DataEntites dll gets pushed up to the Web Service project.
  2. With the app still running, change the OrderDate to be after the ShippedDate.  Note that if you closed the app from above, simply start it again from the start menu so you get the version that doesn’t have the updated DataEnties on the client.  Notice that you don’t see any errors in the DataGridView.  
  3. Press the save button.  You may notice a slight delay as the Web Service re-computes the updated assembly, but when it returns you should now see ErrorProviders indicating that the ShipDate can’t be before the OrderDate.  

 

Updating the client

To update the client, simply republish the Client project using the ClickOnce deployment wizard and restart your client app.  

 

Wrapping Up

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…

 

Steve

 

Posted by Steve.Lasker | 17 Comments
Filed under: ,

Relation Dialog: Relation Only, or with Foreign Key Constraint?

Using the Relation Dialog, you are asked to choose Relation Only, Foreign Key Constraint Only, or Both.  Which is the right choice? 

“Relation Only” makes available the tables related to the table that you are working with.  This means that if you are working with, for example, both a Customers table with CustomerID as primary key and an Orders table with CustomerID as foreign key, you can establish a relationship based on their common key (CustomerID) by inserting a relation.  Without the relation, the dataset is not able to keep track that CustomerID field in one table is related to the CustomerID field in another table.

Foreign key constraint enables enforcing constraints for what-is-called referential integrity, controlling changes to primary key data.  For example, if you tried to change a CustomerID value in the Customers table, the corresponding CustomerID value in the Orders table may no longer be valid.  A foreign key constraint is able to help prevent this situation.  With the foreign key constraint enabled, a failure will occur if changes are made to primary key values that have corresponding foreign key values.

A foreign key constraint only with no relation is implemented for efficiency, since it will contain fewer methods than one with relation.  However, related tables will not be accessible.

For further information, check out my sources:

Introduction to DataRelation Objects

Foreign Key Constraints

 

Yun Zhou

VSLive San Francisco Presentations

At VSLive I presented two sessions.  One for Occasionally Connected Smart Clients and another on Advancements in Data Access.

 

The Occasionally Connected Smart Client session focused on what we’re working on post VS 2005.  Some of which should be in Orcas, some of which will follow afterwards.  Mostly the session was about providing an overview of the evolving application model to leverage SOA based architecture, but not cripple end users at the same time.  In the session I talked about some of the early thinking on some of the Occasionally Connected Systems compnents.

 

The Advancements in Data Access session focused on a few questions that come up a lot around the VS Data features.  In particular I covered:

  • How to separate your Typed DataSets from your TableAdapters
  • Increasing the performance of Typed DataSet Serialization by skipping schema with DataSet.SchemaSerializationMode = ExcludeSchema
  • Implementing a common interface for TableAdapters, ITableAdapter
  • Doing a hierarchical update within a transactions

Slides & Demos

Recording of the VSLive Session

Enjoy

Steve

Embedding SQL Server Express into Custom Applications

Rob Walters, from the SQL Team, and I worked on an article for working with SQL Express in Windows Forms applications.  There are a couple of quirks when it comes to Click Once that have been included in this MSDN article.

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnsse/html/EmSQLExCustApp.asp

Steve

Building SQL manually considered harmful

I just got done fixing a painful bug. The long and short of it is that I had code like this (simplified a lot for brevity):
Public Sub StoreSyncDate() Dim sql As String = "insert into SyncTimes (ObjectID, SyncDate) values ({0}, '{1}')" sql = String.Format(sql, Me.ID, DateTime.Now) Me.Executequery(sql) End Sub On my machine, sql gets passed as a string like "insert into SyncTimes (ObjectID, SyncDate) values (42, '11/22/2005 11:34:45 AM')". Life is good (ignore the SQL injection security problem for the moment).

Then a co-worker in Ireland emails me to ask why my application has broken. Things were working fine earlier in the month, but now he's getting an unhandled exception. What's going on here? Head over to your Regional settings in the Control Panel and change your region to English (Ireland) for some local flavor: "insert into SyncTimes (ObjectID, SyncDate) values (42, '22/11/2005 11:34:15')". There's no month 22, SQL complains, life is not good. The even trickier part is before the 13th of the month, dd/mm/yyyy strings turn in to valid mm/dd/yyyy strings. This is probably worse since it's subtle data corruption rather than an obvious error.

So the moral of the story is, be smarter than me. You never know what will go wrong when you're not using parameterized queries.

Ryan Cavanaugh

How do I get to the generated code?

In order to get to the code that is auto-generated by Visual Studio, follow these steps:
            1. In the menu, select View -> Solution Explorer
            2. In Solution Explorer, click the “Show all Files” icon at the top
            3. Expand the file (e.g. Dataset1.xsd) that you want the generated code for 
            4. Double click on the corresponding Designer file (e.g. Dataset1.Designer.vb) to open up the generated code

From there, you can read and edit the underlying code.

~ Yun Zhou

Posted by SmartClientData | 1 Comments
Filed under:

Data Sources window is inactive in Design View

On certain machines, after installing Visual Studio 2005, the contents of the Data Sources window disappears when a design surface that allows drag and drop from the Data Sources window is active. For example, the Windows Forms designer, User Control Designer, etc…

The cause of this issue is how VS processes the list of associated controls for each data type in the Data Sources window. When users customize the controls in the Data Sources window the settings are saved in the WinFormControls.xml file in the users application data folder. The version numbers in this file have been changed between beta releases and the RTM release and this mismatch of version numbers is the cause of the disabled Data Sources window. Basically this means if you customized the list in a pre-release version of Visual Studio and then install the released version on the same machine you might run into this problem as the installer for Visual Studio does not overwrite or remove this file during setup.

To fix this you have the following 2 options:

Option 1 fixes the problem but no customization is saved and the Data Sources window returns to its original state just like you installed a fresh product:

1)      Close Visual Studio

2)      Delete <DriveLetter>:\Documents and Settings\<UserName>\Application Data\Microsoft\Visual Studio\8.0\DataBindingSetting\WinFormControls.xml
Note:
For Express versions use:
<DriveLetter>:\Documents and Settings\<UserName>\Application Data\Microsoft\VBExpress\8.0\DataBindingSetting

Or, you can simply search for WinFormControls.xml under <DriveLetter>:\Documents and Settings\<UserName>\Application Data\Microsoft\

Option 2 fixes the problem and attempts to preserve any customizations you applied to the Data Sources window previously:

1)      Close Visual Studio

2)      Open the xml file (<DriveLetter>:\Documents and Settings\<UserName>\Application Data\Microsoft\Visual Studio\8.0\DataBindingSetting\WinFormControls.xml)

Note: see above for other SKU's

3)      Check if the version numbers of System.Windows.Forms are 2.0.0.0 (this is the RTM version)

4)      If the version number is not 2.0.0.0 replace the version number with 2.0.0.0

5)      If the version number is 2.0.0.0 delete the file (unfortunately prior settings will be lost, sorry…)

We apologize for any inconvenience this may be causing developers who have installed previous Betas or CTP versions of Visual Studio to help provide early feedback of the product. 

Posted by SmartClientData | 14 Comments
Filed under:

Installing Northwind on SqlExpress

When performing a default install of Visual Studio 2005 an instance of Sql Server Express edition is installed. Unfortunately there are no sample databases included with Sql Express. (http://lab.msdn.microsoft.com/express/sql/)

I've had more than a few people ask how to get the Northwind sample database loaded into Sql Express so here are some step-by-step directions for getting Northwind up and running so you can start trying out all the great new data features in VS.

1) Navigate to the following URL and lick the Download button - http://www.microsoft.com/downloads/details.aspx?familyid=06616212-0356-46a0-8da2-eebc53a68034&displaylang=en

2) Complete the download and install process. This places the necessary files for Northwind (and Pubs) into the "c:\Sql Server 2000 Sample Databases" folder.

3) Open a command window. (Go to Start, Run, type Command and click OK)

4) Change the directory to the sample databases (type:  cd c:\SQL Server Sample Databases)

5) Type or paste the following into the command window replacing computerName with the actual name of your computer:

   osql -E -S computerName/SqlExpress -i instnwnd.sql

6) now go to VS and create a data connection using the computerName\sqlexpress as the Server name in the Add Connection dialog. The Northwind database should now be available.

 

-Steve Stein

 

 

 

 

Returning the identity column value from a TableAdapter DBDirect method

A common scenario when using tables with an auto-increment primary key is to get the value of the identity column for the row you just inserted. Sometimes you can't, or don't want to, use DataSets, which is why we created the DBDirect methods on the TableAdapter. However, the default INSERT function doesn't return the primary key value. How can we get this in one step?

I've created a Visual Basic Console Application and added a DataSource with the Orders table from the SQL Northwind database. The first thing I'm going to do is make a custom INSERT query on the OrdersTableAdapter. Go to the Northwind.xsd file, right-click on OrdersTableAdapter, and choose Add -> "Query...". The TableAdapter Query Wizard will appear. Choose "Use SQL Statements" since we won't be using stored procedures in this example. Select "INSERT" for the query type. Now the default SQL statement appears:
INSERT INTO [dbo].[Orders] ([CustomerID], [EmployeeID], [OrderDate], [RequiredDate], [ShippedDate], [ShipVia], [Freight], [ShipName], [ShipAddress], [ShipCity], [ShipRegion], [ShipPostalCode], [ShipCountry]) VALUES (@CustomerID, @EmployeeID, @OrderDate, @RequiredDate, @ShippedDate, @ShipVia, @Freight, @ShipName, @ShipAddress, @ShipCity, @ShipRegion, @ShipPostalCode, @ShipCountry); SELECT OrderID, CustomerID, EmployeeID, OrderDate, RequiredDate, ShippedDate, ShipVia, Freight, ShipName, ShipAddress, ShipCity, ShipRegion, ShipPostalCode, ShipCountry FROM Orders WHERE (OrderID = SCOPE_IDENTITY())
Let's delete the bottom half since we won't be using this query to refresh a DataSet:
INSERT INTO [dbo].[Orders] ([CustomerID], [EmployeeID], [OrderDate], [RequiredDate], [ShippedDate], [ShipVia], [Freight], [ShipName], [ShipAddress], [ShipCity], [ShipRegion], [ShipPostalCode], [ShipCountry]) VALUES (@CustomerID, @EmployeeID, @OrderDate, @RequiredDate, @ShippedDate, @ShipVia, @Freight, @ShipName, @ShipAddress, @ShipCity, @ShipRegion, @ShipPostalCode, @ShipCountry);
That's a bit more manageable. Now comes the magic part. We'll add a statement to return the @@IDENTITY value:
INSERT INTO [dbo].[Orders] ([CustomerID], [EmployeeID], [OrderDate], [RequiredDate], [ShippedDate], [ShipVia], [Freight], [ShipName], [ShipAddress], [ShipCity], [ShipRegion], [ShipPostalCode], [ShipCountry]) VALUES (@CustomerID, @EmployeeID, @OrderDate, @RequiredDate, @ShippedDate, @ShipVia, @Freight, @ShipName, @ShipAddress, @ShipCity, @ShipRegion, @ShipPostalCode, @ShipCountry); SELECT @@IDENTITY;
I named the new function "InsertAndReturnIdentity". There's one last important piece: select the new query in the DataSet Designer and change the ExecuteMode property from NonQuery to Scalar. We'll explain the difference between the two in a later entry.

Now we're ready to code. We'll create a new TableAdapter and insert a row with a bunch of dummy values:
Sub Main() Dim ordersTA As New NorthwindDataSetTableAdapters.OrdersTableAdapter Dim id As Integer id = ordersTA.InsertAndReturnIdentity("ALFKI", Nothing, Nothing, _ Nothing, Nothing, Nothing, Nothing, "", "", "", "", "", "") Console.WriteLine("The new OrderID is {0}", id) End Sub

The output is:
The new OrderID is 11083
And that's all there is to it!
- Ryan Cavanaugh

Why is my column named _Region in the Typed DataSet?

One quirk you'll find when making a typed DataSet with a column or table named "Region" is that the generated code produces a property called "_Region".

What's up with that? If you read Raymond Chen's blog, you already have a pretty good guess: Backward compatability.

In previous versions of Visual Studio, we did the same thing. Why did we do it back then? When we generated a typed DataSet, we ask the Code DOM provider (a generic way of producing semantically equivalent code in multiple languages) if a property name is a valid identifier. This is important so that when you have a column named "Class", we generate "_Class" and the DataSet still compiles. Before Visual Studio 2005, Code DOM provider told us that "Region" was not a valid identifier, so we helpfully fixed up the to be "_Region". The catch is, "Region" is a valid identifier in VB. In the latest version, the Code DOM was fixed to answer the question correctly.

All is good so far - a bug got fixed in the product, so everyone should be happier, right?. Until you migrate your VS 7.1 DataSet with a column called "Region" to VS 8.0 and regenerate the designer code. The result is that all of your code which referenced _Region is no longer valid. Life isn't so good now. In the end we decided that the best thing to do was special-case the name in the generator so that compatability would be maintained. Since there's no difference between "old" DataSets and "new" ones, people writing new code still get to see this (very minor and rare) oddity. Such is the price we pay for compatability

Bonus trivia: We detect this case for another name: "ExternalSource" also gets a free underscore in the DataSet generator.

- Ryan Cavanaugh

Case sensitive or not sensitive

In a Dataset, case sensitive seems only apply to the data not to the schema

Within a dataset, table and column names are by default case-insensitive — that is, a table in a dataset called "Customers" can also be referred to as "customers." This matches the naming conventions in many databases, including the default behavior of SQL Server, where the names of data elements cannot be distinguished only by case.

Look at carefully for the following code:

VB ------------------------------------------

            Dim ds As DataSet =  New DataSet()
            Dim t1 As DataTable =  New DataTable("ABC")
            Dim t2 As DataTable =  New DataTable("abc")
            ds.Tables.Add(t1)
            Dim b1 As Boolean =  ds.Tables.Contains("Abc")
            ' return true
            Dim t3 As DataTable =  ds.Tables("Abc")
            ' return ABC
 
            ds.Tables.Add(t2)
            Dim b2 As Boolean =  ds.Tables.Contains("ABC")
            Dim b3 As Boolean =  ds.Tables.Contains("abc")
            Dim b4 As Boolean =  ds.Tables.Contains("Abc")
            Dim t4 As DataTable =  ds.Tables("ABC")
            Dim t5 As DataTable =  ds.Tables("abc")
            ' this would throw dim t6 as DataTable = ds.Tables["Abc"]

C#-------------------------------- 

DataSet ds = new DataSet();

DataTable t1 = new DataTable("ABC");

DataTable t2 = new DataTable("abc");

ds.Tables.Add(t1);

bool b1 = ds.Tables.Contains("Abc");

// return true, case  insensitive

DataTable t3 = ds.Tables["Abc"];

// return ABC

ds.Tables.Add(t2);

bool b2 = ds.Tables.Contains("ABC"); // true

bool b3 = ds.Tables.Contains("abc"); // true

bool b4 = ds.Tables.Contains("Abc"); // false now -- Case sensitive now  

DataTable t4 = ds.Tables["ABC"]; // ok

DataTable t5 = ds.Tables["abc"]; // ok

// this would throw DataTable t6 = ds.Tables["Abc"]

Karol and I have encountered this in our dataset designer and this is by design from DataSet team.  So you need to be careful not be fooled. We alway treat the table name in a case sensitive way with our own version of Contains functions like:

   public static bool ContainsTable( DataTableCollection tables, string name ) {

      if( tables.Contains(name) ) {

         DataTable table = tables[name];

         if( StringUtil.EqualValue(table.TableName, name) ) {

            return true;

         }

      }

      return false;

  }

This case sensitive issue applies to DataColumn, DataRelation and Constrains too.

JohnChen (Zhiyong)

Smart Client Data Team

Performance tip for databound UI

When loading large amounts of data into a dataset table with databound UI, things can get pretty slow. One reason is that each time a row is added to the table, the databinding components will perform some work and tell the databound controls that a new record was added. To improve performance, you can suspend the binding events while the data is being loaded. The BindingSource component has a RaiseListChangedEvents property for that purpose. Here's an example how to use it:

    AddressBindingSource.RaiseListChangedEvents = False
    AddressTableAdapter.Fill(AdventureWorksDataSet.Address)
    AddressBindingSource.RaiseListChangedEvents = True
    AddressBindingSource.ResetBindings(False);

Another option is to disconnect the BindingSource component while the Fill is being executed by setting its DataSource property to Nothing and reconnect it after Fill has completed.

HTH

Antoine
Visual Studio Data Design-time

More Posts Next page »
 
Page view tracker