Working with an ObjectDataSource
Prologue
Unrelated (for a change... as of yet) to the website redesign that I'm doing of our company website, I've been reading and doing the code from a very good book called "ASP.Net 2.0 Programming: Problem-Design-Solution". In this book it details how to create a scalable website using the ObjectDataSource and an appropriate n-layer data solution. The ObjectDataSource is very powerful, but it isn't without it's little hitches. I ran into one and it took me a while to find the solution (much of my info is take from there). But I did finally find the answer.
Problem
ObjectDataSource is a very easy way to create a custom object holding your data and then have something else do the hard work of allowing edits, updates etc to your data from the GridView it's bound to. What I found though is that you have to be careful with your variable and parameter ID's.
Solution
Ok, we've got a little setup to do so that we can properly use our ObjectDataSource (ODS). Rather than creating a database, I'm going to simulate one, which means that our updates won't work quite properly (to keep the code down), but it will give an opportunity to see the problem that we're discussing here and let us see some basics of setting up an ObjectDataSource. Ok, to begin, we'll create 2 class modules: ODSCustomer.vb and ODSDal.vb. ODSCustomer will actually be our Business Logic Layer (BLL) and our Presentation Layer Object, and will be an instance of a single customer. Within this instance, are all the properties of a customer and all the functions related to using the customer (typically CRUD create, retrieve, update and delete functions). The ODSDal is our Data Access Layer (DAL). This will actually "retrieve" our data from the database (create the data in our case) and return an array of the Customer objects. (As a side note, in the book, one more module is created and that holds only the details of the customer (properties). This is the information passed between the DAL and the BLL, the common thread so to speak. It is then reformatted by the BLL into an object with the methods etc. We're cheating and having our data returned in our BLL format. Confused yet?) The design is the most confusing part, see the table below for a better idea:
| True N-Tier Design |
| DAL Contains data access code, returns an array of our BLL object | BLL Contains the information of 1 object (1 customer's details). This ensures that the information passed between DAL and presentation Layers is pretty generic. (DAL doesn't know what happens in object, and object doesn't know about data retrieval in DAL). | Object Contains all the information found in the BLL, but also has methods relating to the object (our CRUD methods - they call the DAL and pass the BLL object.). These methods call the DAL, receive and return BLL objects. |
| Our Design |
DAL This generates our data (not retrieves it) and returns an array of our Object | BLL/Object We just combined the two, our DAL returns an array of the object instead of packing up the details to transfer back and forth. There is no abstraction between DAL And BLL. |
Ok, so let's start by creating our DAL. Open our ODSDal.vbs. Lets start with adding a namespace and import the Collections.Generic namespace:
Imports System.Collections.Generic
Namespace ODS.DAL
End Namespace
Next, let's create an array to hold our list of customers, we'll actually create a generic list of that we can pass around:
Public Shared theCustomers As List(Of ODS.Model.ODSCustomer) _
= New List(Of ODS.Model.ODSCustomer)
This creates an empty collection of our customer objects this going to be the datasource that our project uses (essentially). We are actually using Shared functions throughout so we don't necessarily need to create a constructor to generate our list, but we do need to generate it when we retrieve our list. So to create our collection, we have the GenerateCustomers() method:
Public Shared Sub GenerateCustomers()
If theCustomers.Count > 0 Then Exit Sub
Dim iMax As Integer = CInt(New Random().NextDouble() * 100)
theCustomers.Clear()
For iCount As Integer = 0 To iMax
theCustomers.Add(New ODS.Model.ODSCustomer _
(iCount, "FirstName" & iCount.ToString(), _
"LastName" & iCount.ToString()))
Next
End Sub
Basically, this just picks a random number from 1 to 100 and generates that number of customer records. Names are just FirstName + the current item number, same with the last name. This is all stored in a customer object and added to our collection. Now we need to create functions to Get, Update and Delete Customers:
Public Shared Function GetCustomers() As List(Of ODS.Model.ODSCustomer)
GenerateCustomers()
Return theCustomers
End Function
Public Shared Function UpdateCustomer(ByVal ID As Integer, _
ByVal FName As String, ByVal LName As String) As Boolean
theCustomers(ID).FirstName = FName
theCustomers(ID).LastName = LName
End Function
Public Shared Function DeleteCustomer(ByVal ID As Integer) As Boolean
Return theCustomers.Remove(theCustomers(ID))
End Function
You'll see that GetCustomers calls our GenerateCustomers() method and then returns the list of Customers that we just created. UpdateCustomer() takes the ID of the customer and the new Names, and just changes them (FYI, update code doesn't necessarily update the correct name). Delete takes the ID and then removes it from the collection (again, ID doesn't necessarily correspond to placement in the grid so it doesn't always delete the correct entry - that's what our BLL should do...). That's it for our DAL. You should be getting an alert that ODS.Model.ODSCustomer doesn't exist, we'll create that now. Now let's concentrate on our Customer Object. Again, we want to import the collections.generic namespace and create our own namespace:
Imports System.Collections.Generic
Namespace ODS.Model
End Namespace
Now, we need to create properties and the corresponding internal variables to hold the customer's ID, First and Last names, so add:
Private _CustomerID As Integer = 0
Private _FirstName As String = ""
Private _LastName As String = ""
Public Property CustomerID() As Integer
Get
Return _CustomerID
End Get
Set(ByVal value As Integer)
_CustomerID = value
End Set
End Property
Public Property FirstName() As String
Get
Return _FirstName
End Get
Set(ByVal value As String)
_FirstName = value
End Set
End Property
Public Property LastName() As String
Get
Return _LastName
End Get
Set(ByVal value As String)
_LastName = value
End Set
End Property
This is really nothing more than exposing the properties of our object. Ok, now we need to add a constructor to the class so that we can instantiate the object. Well add an overloaded constructor, one that takes no parameters, and one that takes them all:
Public Sub New()
End Sub
Public Sub New(ByVal CustomerId As Integer, _
ByVal FirstName As String, ByVal LastName As String)
Me.CustomerID = CustomerId
Me.FirstName = FirstName
Me.LastName = LastName
End Sub
Now there's one last piece we need to do. We need to have the object invoke its own methods so that it can perform its DAL functions. In order for this object to be used as a datasource, it needs to be able to call its own CRUD methods. So we'll add the following: GetCustomers, UpdateCustomer and DeleteCustomer. If we wanted to, we could add others such as InsertCustomer, and GetCustomerByID (for a details view), but we don't want to. So add the following:
Public Shared Function GetCustomers() As List(Of ODSCustomer)
Return DAL.ODSDal.GetCustomers()
End Function
Public Shared Function UpdateCustomer(ByVal ID As Integer, _
ByVal FName As String, ByVal LName As String) As Boolean
Return DAL.ODSDal.UpdateCustomer(ID, FName, LName)
End Function
Public Shared Function DeleteCustomer(ByVal CustomerID As Integer) As Boolean
Return DAL.ODSDal.DeleteCustomer(CustomerID)
End Function
Basically, these just pass the information back to the DAL and it does the work.
Ok, now for the final piece, we need to create the page that will display our data. Create an ASP.Net page named ObjDataSource.aspx. Once it's created, we don't even have to type any code, we can just do it visually (awesome!). Drop a GridView onto your page, and name it gvwCustomer. Drop an ObjectDataSource onto the page as well, and name it objCustomers. Now click on objCustomers, and click the SmartTag arrow. Select 'Configure Data Source'. You want to choose your Business Object, in our case we want to select ODS.Model.ODSCustomer - you may need to uncheck the 'Show only data components' checkbox - and click next. Now we need to tell it what functions to use for each of our CRUD statements. For Select, choose our GetCustomers() method, for Update, choose our UpdateCustomer method and for Delete select our DeleteCustomer method, then click Finish.
Next, lets configure the GridView. Click on the SmartTag for the GridView and select ObjCustomers as our datasource. Also, check the boxes for Enable Editing and Enable Deleting. Then run your application. Viola, a datasource built from a collection of objects, or an ObjectDataSource.
Ok, so it looks good, but I ran into an issue that I have already built into our project so I could talk about it. I looked everywhere on the Internet and only found on decent solution, so the entire reason I did this post was so that I could add a second. To see our issue, click Delete next to a row, it works fine, but now click 'edit', make a change and then click 'update'. Notice that we get an error that reads 'objCustomers could not find a non-generic method 'UpdateCustomer' that has parameters: ID, FName, LName, FirstName, LastName, CustomerID' (maybe in slightly different order). What's the problem? Simple actually, but frustrating a lot. We were inconsistent with our parameter and variable names. The property names in our Customer class are CustomerID, FirstName and LastName, but if you look at the UpdateCustomer method our parameters are ID, FName and LName.
Ok, I hear you saying, "so what?", that's what I thought too, but it matters. If we aren't consistent then ASP.Net will look for ANOTHER parameter with the same name. Notice that our error statement has 6 parameters, not the 3 we created. It's trying to pass 6 parameters to a method that has only 3. It sees the different names as DIFFERENT parameters and tries to pass all 6. The solution is that we need to modify our UpdateCustomer method so that the parameters are consistent with our property names. Change your code as follows:
Public Shared Function UpdateCustomer(ByVal CustomerID As Integer, _
ByVal FirstName As String, ByVal LastName As String) As Boolean
Return DAL.ODSDal.UpdateCustomer(CustomerID, FirstName, LastName)
End Function
Run your application and edit again. Same error. Why? We need to go back and update our objCustomers Schema. Click on objCustomers and click 'Configure Datasource'. Our ODSCustomer should already be selected so click next. Notice that our Select tab has our GetCustomers method already, it hasn't changed so it doesn't need to. Click on the Update tab and notice that nothing is selected. The previous selection no longer exists, so we need to select our "new" method. Delete should be good also since we made no changes. Now click finish and run the application. Now it works like it should (ok, at least without throwing an exception...).
Epilogue
ObjectDataSource is a very cool new feature to ASP.Net 2.0 and has a lot of potential. Just remember that you have to be consistent in your use of variables and parameters.
If you want a more in depth article about ObjectDataSource, you can find a more comprehensive article (the one I referenced to find the solution to my problem) here.