They Thought They Could Stop Me.

I was working with a DataGrid that had a ButtonColumn in it.  I had a need to set the CommandArgument for this button.  Did you know there is no way to set a CommandArgument for a ButtonColumn?

image

I was all prepared to grab that control and set that property in the ItemDataBound event, but it doesn’t seem to exist.  Most people would resort to a template column, stick a button in it and work on that control.  Problem was, I was doing everything in code with no markup.  That adds a little complexity to that alternative.

Setting a simple breakpoint in the ItemDataBound event, I looked a little closer at what I had to work with in the Immediate window.

? e.Item.Controls(3)
{System.Web.UI.WebControls.TableCell}
    System.Web.UI.WebControls.TableCell: {System.Web.UI.WebControls.TableCell}
? e.Item.Controls(3).Controls.Count
1
? e.Item.Controls(3).Controls(0)
{Text = "Edit"}
    System.Web.UI.WebControls.DataGridLinkButton: {Text = "Edit"}

Hmm, it’s a DataGridLinkButton.  And it does have a CommandArgument property.  So let’s find that control and cast to that type and set that property.

image

I see.  So this type is not user-accessible.  It doesn’t even show up in the Object Browser.  However, it does show up in Reflector, and I can see that it inherits from LinkButton, which is public.  Let’s whip up a quick function to find that control and return a LinkButton for setting the CommandArgument.

Woah, slow down a bit.  This is a ButtonColumn and it can be a link button, command button, or an image button.  If we have a function specifically for LinkButton, it’s going to potentially error out.  In the typical, excellent design of the .NET framework, these three button types are all related using the IButtonControl interface, which has properties for CommandName and CommandArgument.  So by using the interface instead of the exact type, we’re being safe and future-proofing ourselves against other button types.

Private Function GetButtonColumnButton(row As DataGridItem, commandName As String) As IButtonControl
    Return RecurseRowControls(row, commandName)
End Function

Private Function RecurseRowControls(ctl As WebControl, commandName As String) As IButtonControl
    Dim btn As IButtonControl

    ' loop through embedded controls
    For Each c As WebControl In ctl.Controls
        btn = TryCast(c, IButtonControl)

        ' if it is a button and the command name matches, return it
        If btn IsNot Nothing AndAlso String.Compare(btn.CommandName, commandName, True) = 0 Then
            Return btn
        End If

        ' if the control has child control, search them for the button
        If c.HasControls Then
            btn = RecurseRowControls(c, commandName)
            If btn IsNot Nothing Then Return btn
        End If
    Next

    ' no button found
    Return Nothing

End Function

And just like that, we can now have access to the button’s properties like Text, CommandName, CommandArgument, and CausesValidation.  That’s some great stuff there.

Private Sub Grid_ItemDataBound(sender As Object, e As DataGridItemEventArgs) Handles Me.ItemDataBound
    Dim btn As IButtonControl

    If e.Item.ItemType = ListItemType.AlternatingItem Or e.Item.ItemType = ListItemType.Item Then
        btn = GetButtonColumnButton(e.Item, "Edit")
        btn.CommandArgument = "something like an ID"
        btn.Text = "specific text label"
    End If
End Sub

Now This is Something Worth Looking Into

When you see something like this in the call stack, you have to know more:

[InvalidOperationException: This SqlTransaction has completed; it is no longer usable.]
   System.Data.SqlClient.SqlTransaction.ZombieCheck() +1623536
   System.Data.SqlClient.SqlTransaction.Rollback() +172
   Framework.Common.Database.RollbackTransaction() in C:\Framework\Common\Database.vb:413 
   Test.uxCreate_Click(Object sender, EventArgs e) in C:\Test.aspx.vb:47
   System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument) +154
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3691

And the error message is interesting as well: “This SqlTransaction has completed; it is no longer usable”.

A little background on what is going on: this is a database class that is managing a SQL transaction to be used by multiple objects.  The database object instance gets passed along to the different objects and they use it, participating in the transaction.  Somewhere along the way, maybe a commit is occurring and then the transaction becomes invalid.  That’s how it seems.

Tossing in a bunch of debug.writelines to see what what happening inside the methods popped up a message about a SQLException being raised because of a non-existent column name.  Fixing the schema problem fixed the zombie problem.  But what was the reason for the original error?

Let’s say you bring a sandwich into work.  You give that sandwich to someone and tell them to put it in the refrigerator for you.  This person (the fridgemaster) puts it in the fridge and comes back to the refrigerator a little later to find your sandwich moldy and spoiled, so he throws the sandwich out.  Later on yet, you go to the fridgemaster and ask for your sandwich.  He says “this sandwich has spoiled; it is no longer usable.”  In response to your puzzled look, the fridgemaster says “I did a ZombieCheck and it was moldy.”

In coding terms, the order of events was: TX started, SQL error occurred, TX rolled back (by SQL Server), SQLException raised and caught, TX rolled back in catch block (by user code) and unable to because the TX already was rolled back by SQL Server.  This rollback behavior is dependent on the severity of the error. In the case of the schema error, which presumably was interpreted as “this will NEVER work”, this equated to being severe enough to roll back the transaction.