Reputation: 328
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
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