Blog Home  Home Feed your aggregator (RSS 2.0)  
ObjectDataSource in Depth (Part 1) - Manuel Abadia's ASP.NET stuff
 
# Saturday, February 11, 2006

Since the beginning of the .Net Framework, it was tempting to use the DataSet for a lot of tasks because it is very integrated within the framework and it’s very versatile. However, as the projects become bigger and the domain’s complexity increases, working with DataSets becomes more problematic.

In order to handle more complex scenarios we need to have a solid model based on objects rather than working with low level data as rows and columns. However, it was difficult to integrate our own domain objects into the presentation layer and nearly impossible to obtain an integration level like the DataSet. With the new data binding infrastructure introduced in ASP.NET 2.0 this is easier.

I won’t go into detail about the benefits of using your own domain objects for developing n-tier applications. An excellent source for this is Martin Fowler’s book [1].

ObjectDataSource lets us integrate our own objects in the data binding framework without much work. In order to do so, you have to understand the data binding infrastructure (see my previous posts if you need more info about it) and what ObjectDataSource does for you.

ObjectDataSource needs an object that will take care of the CRUD operations (create, retrieve, update and delete) of your domain objects. You specify the type of this object to the ObjectDataSource using the TypeName property. This object, that can be a data mapper object, a business object or a business façade, has to be designed with some guidelines in mind to be used properly by the ObjectDataSource:

* Each time the ObjectDataSource needs to make a CRUD operation, it will try to find (using reflection) the method to call (of course you need to set some properties to help it).
* If it’s a static method, it will get the parameters and call it.
* If it’s an instance method, it will create an object of the class, it will call the method and then destroy the instance (there is an exception to this, but more about it later).
* If you don’t want the control to create and destroy instances, you can override the default behaviour capturing the Creating and Disposing events. If you supply a value for the argument ObjectInstance, the control will use this object instead of creating a new instance. If you set the Cancel argument of the Disposing event, the control won’t try to dispose the object.

Before you ask me: No, the control doesn't cache reflection data so every time a CRUD operation is performed it has to search for the method and invoke it.

We have to specify our CRUD methods using the properties SelectMethod, InsertMethod, UpdateMethod and DeleteMethod (there’s a property called SelectCountMethod that will be explained later) if we want them to be called by the ObjectDataSource. Of course, if we only want to retrieve data, we only need to specify the SelectMethod (as I explained previously when talking about DataSourceViews, we’re required to specify at least the select method, the others are optional).

ObjectDataSource lets us use two modes for passing parameters for the data mapper’s methods:
* simple types
* custom objects

If we’re using the first mode, the method should have one parameter for each bound value and optionally some extra parameters you can specify (more on this later). In the second mode, the parameter is an object with at least a property for each bound value, and optionally some extra parameters you can specify.

The control has a property called DataObjectTypeName that when it's set specifies that we’re using the second mode, and the object’s type used for the parameter. If we don’t set it or if it’s null or empty we’re using the first mode.

Most of the extra parameters I was talking about can be specified for each CRUD method in a related collection: SelectParameters for the SelectMethod (the SelectMethod can use also the FilterParameters, but I’ll talk more about that later), InsertParameters for the InsertMethod, UpdateParameters for the UpdateMethod and DeleteParameters for the DeleteMethod.

To make things more complicated, the control lets us handle optimistic offline concurrency if we set the property ConflictDetection to ConflictOptions.CompareAllValues instead of the default value ConflictOptions.OverwriteChanges. If we want to use optimistic offline concurrency, the number of parameters passed to the data mapper’s CRUD methods increases (I'll elaborate more on this on a future post).

If that wasn’t enough the control lets us perform sorting, filtering and paging, and even it has some caching support. It can be a bit intimidating the first time you use it but after some practice it becomes easy to use.

To continue the explanation I’ll be showing an example of how to use it. Our goal is to show a list of products and be able to edit or delete one of them using a GridView. Our products will be instances of the Product class:

 

using System;

 

public class Product

{

    #region Fields

 

    protected int _id;

    protected string _name;

    protected string _description;

    protected decimal _price;

 

    #endregion

 

    #region Properties

 

    public int Id

    {

        get { return _id; }

        set { _id = value; }

    }

 

    public string Name

    {

        get { return _name; }

        set { _name = value; }

    }

 

    public string Description

    {

        get { return _description; }

        set { _description = value; }

    }

 

    public decimal Price

    {

        get { return _price; }

        set { _price = value; }

    }

 

    #endregion

 

    #region Constructors

 

    public Product()

    {

        _id = -1;

    }

 

    public Product(int id, string name, string description, decimal price)

    {

        _id = id;

        _name = name;

        _description = description;

        _price = price;

    }

 

    #endregion

}

 

That maps directly to a database table:

CREATE TABLE Products

(

    id int identity(1,1),

    name nvarchar(50) not null,

    description nvarchar(100) not null,

    price decimal(10, 2) not null

)

 

ALTER TABLE Products ADD PRIMARY KEY(id)

 

I’ll be using custom objects for the parameters (the custom Product class) instead of simple types. In this sample I'll use a data mapper class that will be called ProductsDAL. Lets start with the retrieve method (no paging in this article):

public List<Product> LoadAll()

{

    List<Product> products = new List<Product>();

 

    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);

    SqlCommand command = new SqlCommand("SELECT * FROM Products", conn);

 

    conn.Open();

    SqlDataReader dr = command.ExecuteReader();

    while (dr.Read()) {

        Product prod = new Product();

 

        prod.Id = (int)dr["id"];

        prod.Name = (string)dr["name"];

        prod.Description = (string)dr["description"];

        prod.Price = (decimal)dr["price"];

 

        products.Add(prod);

    }

 

    dr.Close();

    conn.Close();

 

    return products;

}

 

The select method can return any of the following types:
* A DataView
* A DataSet
* A DataTable
* An IEnumerable object
* Another object

If a DataSet is returned, the ObjectDataSourceControl will work with a view of the first table in the DataSet. If a DataTable is returned, the control will work with a view of the DataTable. If the object returned from the select method is not a DataView, a DataSet, a DataTable or an ICollection, it is wrapped in an object array of one element and handled as an IEnumerable type.

You may wonder why I didn’t make the LoadAll a static method. The answer is because I need an instance method to support paging efficiently (as I will show later).

Now I have to create a new web form, so I add an ObjectDataSource and configure it using the smart tag: In the first dialog I choosed the ProductsDAL class and in the Select tab, the LoadAll method. After clicking OK we can look at the generated markup to see that what the wizard did for us was to set the TypeName property to ProductsDAL and the SelectMethod to LoadAll.

After that, drag a GridView to the form, choose the ObjectDataSource as its data source, then set AutoGenerateColumns to false and go to the columns collection to set the following order for the columns: id, name, description and price. Set the id column to be read only and close the columns editor. The last thing to do is to set the GridView’s DataKeyNames property to Id, as the Product’s table primary key is the id column.

Before running the application insert some test values (I know I’m original ;-)):

INSERT INTO Products(name, description, price) VALUES ('Product 1', 'Description 1', 1)

INSERT INTO Products(name, description, price) VALUES ('Product 2', 'Description 2', 2)

INSERT INTO Products(name, description, price) VALUES ('Product 3', 'Description 3', 3)

INSERT INTO Products(name, description, price) VALUES ('Product 4', 'Description 4', 4)
...

 

You should see a table with the products if you run the application.

To update the database we code the following method in the ProductsDAL class:

public static void Edit(Product prod)

{

    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);

    SqlCommand command = new SqlCommand("UPDATE Products SET name=@name, description=@description, price=@price WHERE id=@id", conn);

    command.Parameters.Add(new SqlParameter("@name", prod.Name));

    command.Parameters.Add(new SqlParameter("@description", prod.Description));

    command.Parameters.Add(new SqlParameter("@price", prod.Price));

    command.Parameters.Add(new SqlParameter("@id", prod.Id));

 

    conn.Open();

    command.ExecuteNonQuery();

    conn.Close();

}

 

In order to modify the example to support updating we can use the wizard again to update the ObjectDataSource or better specify the following properties:
 UpdateMethod = Edit
 DataObjectTypeName = Product

The select method is the same for both modes to pass parameters. However, the insert, update and delete method are different and that’s why it’s needed to specify the DataObjectTypeName now. Every time they’re called new instances of that type are created using the default constructor.

We have to set GridView’s AutoGenerateEditButton property to true in order to allow the grid to be editable.

If we run the application now we could be lucky enough and be able to edit the value. However, if our current culture does not format decimals the same way the InvariantCulture does, we’ll get an error:
Input string was not in a correct format.
[…]
1,00 is not a valid value for Decimal.
[…]

If we are having this problem write the following at the top of the Page_Load method:

System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;

 

This is a hack but will let us continue using the example. Soon I’ll blog about the proper way to fix this.

To delete a product we need to add another method to the ProductsDAL class:

public static void Delete(Product prod)

{

    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);

    SqlCommand command = new SqlCommand("DELETE Products WHERE id=@id", conn);

    command.Parameters.Add(new SqlParameter("@id", prod.Id));

    conn.Open();

    command.ExecuteNonQuery();

    conn.Close();

}

 

And set ObjectDataSource’s DeleteMethod to Delete and GridView’s AutoGenerateDeleteButton to true.

If we set a break point in the Delete method you’ll notice that even a Product object is passed to the method, the only property that has been set is the Id. That’s by design. The only properties set in the product are the ones specified in the GridView’s DataKeyNames so if we forget to set that property the delete method will fail.

If we prefer to pass parameters to our data mapper using simple types, we have to delete the property DataObjectTypeName and change the method for update and delete, since now it will not accept a Product as the parameter:

public static void Edit(int old_id, string name, string description, decimal price)

{

    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);

    SqlCommand command = new SqlCommand("UPDATE Products SET name=@name, description=@description, price=@price WHERE id=@id", conn);

    command.Parameters.Add(new SqlParameter("@name", name));

    command.Parameters.Add(new SqlParameter("@description", description));

    command.Parameters.Add(new SqlParameter("@price", price));

    command.Parameters.Add(new SqlParameter("@id", old_id));

 

    conn.Open();

    command.ExecuteNonQuery();

    conn.Close();

}

 

public static void Delete(int old_id)

{

    SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString);

    SqlCommand command = new SqlCommand("DELETE Products WHERE id=@id", conn);

    command.Parameters.Add(new SqlParameter("@id", old_id));

    conn.Open();

    command.ExecuteNonQuery();

    conn.Close();

}

 

Recall that in our GridView we had four BoundFields (id, name, description and price) and one was marked as ReadOnly (id). To the Insert, Update and Delete operations, the framework passes some IOrderedDictionary containing the values related to the current operation (I’ll give more details about this in a future post). For the Update operation, there’s a dictionary containing the control values extracted from the Bind expressions that were in the bound control (for more information about this see Data Source Controls - Under the Hood (Part 4)). As a BoundField is just a wrapper around a Bind expression in edit mode and a Eval expression in normal mode, when Update gets called, the framework extracts the following dictionary from the GridView (I’m editing the first product):

{ “name”, “Product 1”}
{ “description”, “Description 1” }
{ “price”, “1.00” }

As the id was a ReadOnly field, it doesn’t have an underlying Bind expression so when ExtractValues is called on the IBindingTemplate the result doesn’t has an entry for id. However, as we specified the id as primary key for the GridView using the DataKeyNames, when the GridView calls the update method, it also passes the primary keys as parameters. ObjectDataSource has a property called OldValuesParameterFormatString that uses internally to format some dictionary’s entries like the keys, so as we have set OldValuesParameterFormatString="old_{0}", the keys dictionary passed to the Update method after formatting the name is:
{ “old_id”, “1”}

That’s why the Update method takes the following parameters:

int old_id, string name, string description, decimal price

For the Delete method, only the primary key is needed.

I’ll continue talking about the ObjectDataSource next week.

Update:
You can access to the other parts here:

ObjectDataSource in Depth (Part 2)
ObjectDataSource in Depth (Part 3)
ObjectDataSource Limitations, Problems and Possible Solutions

 

[1] Patterns of Enterprise Application Architecture - Martin Fowler

 

Copyright © 2014 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.