Blog Home  Home Feed your aggregator (RSS 2.0)  
Binding, Read Only Properties and DataSourceControls - Manuel Abadia's ASP.NET stuff
 
# Thursday, 14 June 2007

I have heard of this problem very often:

I’m using a DataSourceControl (for example, an ObjectDataSource) and a GridView. I have a read only property and when I try to update an item, I get an exception because the DataSourceControl attempts to set the read only property. What is happening?

Let see an example. In this particular example I’m showing the content of a shopping cart. The GridView has the following columns:

<Columns>

    <asp:BoundField DataField="ProductId" HeaderText="ProductId" ReadOnly="True" SortExpression="ProductId" />

    <asp:BoundField DataField="ProductName" HeaderText="ProductName" ReadOnly="True" SortExpression="ProductName" />

    <asp:BoundField DataField="ProductUnits" HeaderText="ProductUnits" SortExpression="ProductUnits" />

    <asp:BoundField DataField="ProductPrice" HeaderText="ProductPrice" ReadOnly="True" SortExpression="ProductPrice" />

    <asp:BoundField DataField="Total" HeaderText="Total" ReadOnly="True" SortExpression="Total" />

    <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />

</Columns>

Note that all the columns for the GridView are read only except the ProductUnits. The only read only property for a shopping cart item is Total.

If we try the sample, when updating an item in the cart, an exception is thrown because the underlying DataSourceControl tries to update a read only property (Total).  The first reaction is to blame the DataSourceControl for the error but after some findings the problem resides in the GridView. The DataSourceControl receives a keys dictionary, a values dictionary and an oldValues dictionary in an Update operation. If the data it receives is wrong, then don’t blame it if the operation does not work.

What is the problem? Well, I thought that a BoundField was a shortcut to save time and space to bind a property. That it generates a Label in normal mode and a TextBox in Edit mode. The label will show the value of the DataField and the TextBox has two way data binding for the specified DataField. If you select a BoundField column and then click on “Convert this field on a TemplateField”, you can see that the generated templates match what was expected.

If the BoundField has the property ReadOnly set to true, you expect that in Edit mode a label will be shown instead of a TextBox. If you use select that BoundField and then click on “Convert this field on a TemplateField” you’ll see that the generated templates match what was expected too.

With this configuration that only uses TemplateFields instead of BoundFields:

<Columns>

    <asp:TemplateField HeaderText="ProductId" SortExpression="ProductId">

        <EditItemTemplate>

            <asp:Label ID="Label1" runat="server" Text='<%# Eval("ProductId") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label1" runat="server" Text='<%# Bind("ProductId") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:TemplateField HeaderText="ProductName" SortExpression="ProductName">

        <EditItemTemplate>

            <asp:Label ID="Label2" runat="server" Text='<%# Eval("ProductName") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label2" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:TemplateField HeaderText="ProductUnits" SortExpression="ProductUnits">

        <EditItemTemplate>

            <asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductUnits") %>'></asp:TextBox>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label3" runat="server" Text='<%# Bind("ProductUnits") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:TemplateField HeaderText="ProductPrice" SortExpression="ProductPrice">

        <EditItemTemplate>

            <asp:Label ID="Label3" runat="server" Text='<%# Eval("ProductPrice") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label4" runat="server" Text='<%# Bind("ProductPrice") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:TemplateField HeaderText="Total" SortExpression="Total">

        <EditItemTemplate>

            <asp:Label ID="Label4" runat="server" Text='<%# Eval("Total") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label5" runat="server" Text='<%# Bind("Total") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />

</Columns>

The update operation doesn’t give any problem with the read only property “Total”.  Why?

Imagine, I’m updating the units from 1 to 5 for a shopping cart item with the values ProductId=1, ProductName=”Product 1”, ProductUnits=1, ProductPrice=2, and Total=2.

When I use the above configuration for the GridView, it calls the Update method on the DataSourceControl with the following parameters:

Keys

keys.png

Values

values.png

oldValues

oldValues2.png

If I use the initial configuration using BoundFields, the oldValues dictionary is different:

oldValues.png

When the GridView is in Edit mode and the data binding is performed, it fills a private dictionary called BoundFieldValues. In the HandleUpdate method, the oldValues dictionary that will be passed to the DataSourceControl is filled with the BoundFieldValues. However, the BoundFieldValues dictionary is populated using a template method of the DataControlField class called ExtractValuesFromCell. For a TemplateField, the ExtractValuesFromCell adds the value extracted from the Bind expression (see this post for more information) to the dictionary. If a cell uses an Eval expression instead of a Bind expression, the value doesn’t get added to the dictionary. However, for a BoundField, the ExtractValuesFromCell adds the value to the dictionary even if it is read only, because it is called with a parameter includeReadOnlyFields = true.

The values dictionary is filled calling ExtractValuesFromCell to the current row being edited, but this time, the includeReadOnlyFields is false for the BoundField.

The keys dictionary is filled with the data from the DataKeys property.

If you use this configuration for the GridView:

<Columns>

    <asp:TemplateField HeaderText="ProductId" SortExpression="ProductId">

        <EditItemTemplate>

            <asp:Label ID="Label1" runat="server" Text='<%# Bind("ProductId") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label1" runat="server" Text='<%# Bind("ProductId") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:TemplateField HeaderText="ProductName" SortExpression="ProductName">

        <EditItemTemplate>

            <asp:Label ID="Label2" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label2" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:TemplateField HeaderText="ProductUnits" SortExpression="ProductUnits">

        <EditItemTemplate>

            <asp:TextBox ID="TextBox1" runat="server" Text='<%# Bind("ProductUnits") %>'></asp:TextBox>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label3" runat="server" Text='<%# Bind("ProductUnits") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:TemplateField HeaderText="ProductPrice" SortExpression="ProductPrice">

        <EditItemTemplate>

            <asp:Label ID="Label3" runat="server" Text='<%# Bind("ProductPrice") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label4" runat="server" Text='<%# Bind("ProductPrice") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:TemplateField HeaderText="Total" SortExpression="Total">

        <EditItemTemplate>

            <asp:Label ID="Label4" runat="server" Text='<%# Bind("Total") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label5" runat="server" Text='<%# Bind("Total") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />

</Columns>

The oldValues dictionary is the identical to the one obtained using BoundFields instead of TemplateFields.

A DataSourceControl usually combines the data from the keys, values and oldValues dictionary to make an update operation, so to make the operation work as expected you should know in advance what values you want to send to the DataSourceControl.

I checked some advanced grid controls and found that Telerik’s RadGrid have more control about this extraction process. This is from the manual of the RadGrid:

Telerik RadGrid can extract values even from columns that are set as read-only, if the column's property ForceExtractValue is set to:

• "InBrowseMode" - when deleting records
• "InEditMode" - when inserting/updating records 
• "Always" - for all modes

The default value for this property is "None", i.e. the extraction of the default values will not be performed only for read-only columns.

To finally answer the question, for an ObjectDataSource, probably you want all the values except the Total to be present in the final update operation, so this configuration is the one you’ll use:

<Columns>

    <asp:BoundField DataField="ProductId" HeaderText="ProductId" ReadOnly="True" SortExpression="ProductId" />

    <asp:BoundField DataField="ProductName" HeaderText="ProductName" ReadOnly="True"

        SortExpression="ProductName" />

    <asp:BoundField DataField="ProductUnits" HeaderText="ProductUnits" SortExpression="ProductUnits" />

   <asp:BoundField DataField="ProductPrice" HeaderText="ProductPrice" ReadOnly="True"

        SortExpression="ProductPrice" />

    <asp:TemplateField HeaderText="Total" SortExpression="Total">

        <EditItemTemplate>

            <asp:Label ID="Label1" runat="server" Text='<%# Eval("Total") %>'></asp:Label>

        </EditItemTemplate>

        <ItemTemplate>

            <asp:Label ID="Label1" runat="server" Text='<%# Bind("Total") %>'></asp:Label>

        </ItemTemplate>

    </asp:TemplateField>

    <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />

</Columns>

Hopefully this post has clarified a bit the problem, the possible options and the implications, when working with a GridView and a DataSourceControl.

Thursday, 14 June 2007 10:26:00 (Romance Daylight Time, UTC+02:00)  #    Comments [0]   ASP.NET  | 
Copyright © 2018 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.