ByRef Snafu

Posted by Hugh Ang at 1/22/2007 09:26:00 AM

At a client I worked some time ago, a consultant who had worked there prior to me had told the client developers (mostly from VB 6 background) a "best practice": use ByRef when passing reference type parameters and ByVal for value type ones. Besides the obvious confusion of the concepts, this practice and a particular VB.NET compiler design had caused a subtle bug in the application that was difficult to track down. That application was written for .NET framework 1.1 with VS.NET 2003. Now the VB.NET compiler for VS.NET 2005 still does the same so I am able to contrive a simple example to demo this bug in VS.NET 2005.

To get started, I have created a simple form with a textbox "txtName" and a button "btnCallByRef" as in the following:

And here is the code:

Public Class Main

    Private Sub btnCallByRef_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCallByRef.Click


        Debug.WriteLine("txtBox is passed to a function ByRef")

    End Sub


    Private Sub ReadTextBox(ByRef txtbox As TextBox)

        ' nop

    End Sub


    Private Sub txtName_Leave(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtName.Leave

        Debug.WriteLine("txtName_Leave handler")

    End Sub


    Private Sub AnotherHandler(ByVal sender As System.Object, ByVal e As System.EventArgs)

        Debug.WriteLine("another handler")

    End Sub


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

        AddHandler txtName.Leave, AddressOf AnotherHandler

    End Sub

End Class

The textbox "txtName" has a design time handler "txtName_Leave" defined for its "Leave" event. During form load, another handler "AnotherHandler" is added dynamically to handle the same "Leave" event. The client application had a framework, which uses a generic event handler to monitor form all kinds of events - the "AnotherHandler" is just simulating that handler in the framework. Note that the framework's generic handler needs to be called last in the delegate chain as it needs to look at the final attributes of the controls, since they may be changed by handlers that are put there at design time by form developers.

This is all working pretty well when I tab out the textbox to fire the Leave event. See the following in the output window while debugging:

txtName_Leave handler
another handler

well, pretty well until the "btnCallByRef_Click" function is called:

txtBox is passed to a function ByRef
another handler
txtName_Leave handler

When I tab out the textbox to fire the Leave event, the execution order of the handlers has been reversed. What happened here? It turns out that when a control is declared as in the following:

Friend WithEvents txtName As System.Windows.Forms.TextBox

The VB.NET compiler will create a private field "_txtName" and a public property "txtName" encapsulating the field and what's more interesting is the setter of the property as seen in IL:

.method assembly newslot specialname strict virtual 
instance void set_txtName(class [System.Windows.Forms]System.Windows.Forms.TextBox WithEventsValue) cil managed synchronized
.custom instance void [mscorlib]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = ( 01 00 00 00 )
// Code size 93 (0x5d)
.maxstack 3
.locals init (bool V_0)
IL_0000: ldarg.0
IL_0001: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox VBWinForm.Main::_txtName
IL_0006: ldnull
IL_0007: ceq
IL_0009: ldc.i4.0
IL_000a: ceq
IL_000c: stloc.0
IL_000d: ldloc.0
IL_000e: brfalse.s IL_0029
IL_0010: ldarg.0
IL_0011: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox VBWinForm.Main::_txtName
IL_0016: ldarg.0
IL_0017: dup
IL_0018: ldvirtftn instance void VBWinForm.Main::txtName_Leave(object,
class [mscorlib]System.EventArgs)
IL_001e: newobj instance void [mscorlib]System.EventHandler::.ctor(object,
native int)
IL_0023: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::remove_Leave(class [mscorlib]System.EventHandler)
IL_0028: nop
IL_0029: nop
IL_002a: ldarg.0
IL_002b: ldarg.1
IL_002c: stfld class [System.Windows.Forms]System.Windows.Forms.TextBox VBWinForm.Main::_txtName
IL_0031: ldarg.0
IL_0032: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox VBWinForm.Main::_txtName
IL_0037: ldnull
IL_0038: ceq
IL_003a: ldc.i4.0
IL_003b: ceq
IL_003d: stloc.0
IL_003e: ldloc.0
IL_003f: brfalse.s IL_005a
IL_0041: ldarg.0
IL_0042: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox VBWinForm.Main::_txtName
IL_0047: ldarg.0
IL_0048: dup
IL_0049: ldvirtftn instance void VBWinForm.Main::txtName_Leave(object,
class [mscorlib]System.EventArgs)
IL_004f: newobj instance void [mscorlib]System.EventHandler::.ctor(object,
native int)
IL_0054: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::add_Leave(class [mscorlib]System.EventHandler)
IL_0059: nop
IL_005a: nop
IL_005b: nop
IL_005c: ret
} // end of method Main::set_txtName

So it can be seen that the existing handler is removed, the field _txtName is set and the event handler is added back again. And if we take a look at the IL code of btnCallByRef_Click. Note how the "set_txtName" is called after the "ReadTextBox" call as a result of passing ByRef:

.method private instance void btnCallByRef_Click(object sender,
class [mscorlib]System.EventArgs e) cil managed
// Code size 38 (0x26)
.maxstack 2
.locals init ([0] class [System.Windows.Forms]System.Windows.Forms.TextBox VB$t_ref$S0)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.0
IL_0003: callvirt instance class [System.Windows.Forms]System.Windows.Forms.TextBox VBWinForm.Main::get_txtName()
IL_0008: stloc.0
IL_0009: ldloca.s VB$t_ref$S0
IL_000b: callvirt instance void VBWinForm.Main::ReadTextBox(class [System.Windows.Forms]System.Windows.Forms.TextBox&)
IL_0010: nop
IL_0011: ldarg.0
IL_0012: ldloc.0
IL_0013: callvirt instance void VBWinForm.Main::set_txtName(class [System.Windows.Forms]System.Windows.Forms.TextBox)
IL_0018: nop
IL_0019: ldstr "txtBox is passed to a function ByRef"
IL_001e: call void [System]System.Diagnostics.Debug::WriteLine(string)
IL_0023: nop
IL_0024: nop
IL_0025: ret
} // end of method Main::btnCallByRef_Click

This VB.NET compiler behavior is clearly designed for the convenience of developers so they don't have to manually add or remove handlers in their code. Unfortunately not knowing this and following an ill-advised practice involving ByRef led to a really unexpected bug.