Brandon Hood
Brandon Hood

Reputation: 328

Delayed assignment to a WithEvents backing field

I've noticed that when a property's backing field has the WithEvents modifier, value assignment can "lag" for lack of better words. I've reproduced the behavior in a simple demo, so the purpose of WithEvents won't be evident here (and thus it won't be constructive to say "just get rid of it")

Public Class ItemViewModel
    Public Property Id As Integer
End Class

Public Class ViewModel
    Inherits ViewModelBase

    Private WithEvents _item As ItemViewModel = New ItemViewModel() With {.Id = 0}
    Public Property Item As ItemViewModel
        Get
            Return _item
        End Get
        Set(value As ItemViewModel)
            SetProperty(_item, value)
        End Set
    End Property
...

SetProperty definition:

Protected Function SetProperty(Of T)(ByRef field As T, value As T, <CallerMemberName> Optional name As String = Nothing) As Boolean
    If (EqualityComparer(Of T).Default.Equals(field, value)) Then
        Return False
    End If
    field = value
    NotifyPropertyChanged(name)
    Return True
End Function

When I update the Item property to be a new item with an incremented id, the property getter is hit as soon as the event fires, as expected. However, the value of the backing field is still the old value! If I add another PropertyChanged event right after the SetProperty call, the backing field will have the correct value at that point. Of course, if I take out WithEvents, it works as expected with only one event.

This is the only time I've seen SetProperty fail in such a way. What is the problem that WithEvents is causing?

UPDATE: When ViewModel implements INotifyPropertyChanged directly, instead of inheriting from the base, and raises PropertyChanged after setting the value, it works.

Upvotes: 1

Views: 170

Answers (1)

Jonathan Gilbert
Jonathan Gilbert

Reputation: 3840

What's going on here is that WithEvents is a feature that the .NET Framework itself does not natively support. VB.NET is implementing it on top of .NET. The feature is there because it was also provided by VB6. The way the feature was implemented in VB6, though, is very different because of a fundamental difference in the event models between COM and .NET.

I won't go into how VB6 implemented the feature; that isn't really relevant. What's important is how events work with .NET. Basically, with .NET, events have to be explicitly hooked and unhooked. When events are defined, there are a lot of parallels with how properties are defined. In particular, there is a method that adds a handler to an event and a method that removes a handler, similar to the symmetry between the "set" and "get" methods a property has.

The reason events use methods like this is to hide the list of attached handlers from outside callers. If code outside of a class had access to the full list of attached handlers, it would be possible for it to interfere with it, which would be a very poor programming practice potentially resulting in very confusing behaviour.

VB.NET exposes direct calls to these event "add" and "remove" methods through the AddHandler and RemoveHandler operators. In C#, exactly the same underlying operation is expressed using the += and -= operators, where the left-hand argument is an event member reference.

What WithEvents gives you is syntactic sugar that hides the AddHandler and RemoveHandler calls. What's important to recognize is that the calls are still there, they're just implicit.

So, when you write code like this:

Private WithEvents _obj As ClassWithEvents

Private Sub _obj_GronkulatedEvent() Handles _obj.GronkulatedEvent
  ...
End Sub

..you are asking VB.NET to ensure that whatever object is assigned to _obj (keeping in mind that you can change that object reference at any time), the event GronkulatedEvent should be handled by that Sub. If you change the reference, then the old object's GronkulatedEvent should be immediately detached, and the new object's GronkulatedEvent attached.

VB.NET implements this by turning your field into a property. Adding WithEvents means that the field _obj (or, in your case, _item) is not actually a field. A secret backing field is created, and then _item becomes a property whose implementation looks like this:

Private __item As ItemViewModel ' Notice this, the actual field, has two underscores

Private Property _item As ItemViewModel
  <CompilerGenerated>
  Get
    Return __item
  End Get
  <CompilerGenerated, MethodImpl(Synchronized)>
  Set(value As ItemViewModel)
    Dim previousValue As ItemViewModel = __item

    If previousValue IsNot Nothing Then
      RemoveHandler previousValue.GronkulatedEvent, AddressOf _item_GronkulatedEvent
    End If

    __item = value

    If value IsNot Nothing Then
      AddHandler value.GronkulatedEvent, AddressOf _item_GronkulatedEvent
    End If
  End Set
End Property

So, why does this cause the "lag" you see? Well, you can't pass a property "ByRef". To pass something "ByRef", you need to know its memory address, but a property hides the memory address behind "get" and "set" methods. In a language like C#, you would simply get a compile-time error: A property is not an L-value, so you cannot pass a reference to it. However, VB.NET is more forgiving and writes extra code behind the scenes to make things work for you.

In your code, you are passing what looks like a field, the _item member, into SetProperty, which takes the parameter ByRef so it can write a new value. But, due to WithEvents, the _item member is really a property. So, what does VB.NET do? It creates a temporary local variable for the call to SetProperty, and then assigns it back to the property after the call:

Public Property Item As ItemViewModel
  Get
    Return _item ' This is actually a property returning another property -- two levels of properties wrapping the actual underlying field -- but VB.NET hides this from you
  End Get
  Set
    ' You wrote: SetProperty(_item, value)
    ' But the actual code emitted by the compiler is:
    Dim temporaryLocal As ItemViewModel = _item ' Read from the property -- a call to its Get method

    SetProperty(temporaryLocal, value) ' SetProperty gets the memory address of the local, so when it makes the assignment, it is actually writing to this local variable, not to the underlying property

    _item = temporaryLocal ' Once SetProperty returns, this extra "glue" code passes the value back off to the property, calling its Set method
  End Set
End Property

So, because WithEvents converted your field to a property, VB.NET had to defer the actual assignment to the property until after the call to SetProperty returns.

Hope that makes sense! :-)

Upvotes: 4

Related Questions