延迟分配到WithEvents后备字段



我注意到,当属性的后台字段具有WithEvents修饰符时,由于缺少更好的单词,值分配可能会"滞后"。我在一个简单的演示中复制了这种行为,所以WithEvents的目的在这里不会很明显(因此说"去掉它"是没有建设性的)

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定义:

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

当我将Item属性更新为具有递增id的新项时,正如预期的那样,一旦事件触发,就会命中属性getter。然而,支持字段的值仍然是旧值!如果在SetProperty调用之后立即添加另一个PropertyChanged事件,则backing字段此时将具有正确的值。当然,如果我去掉WithEvents,它只对一个事件起到预期的作用。

这是我唯一一次看到SetProperty以这种方式失败。WithEvents引起了什么问题?

UPDATE:当ViewModel直接实现INotifyPropertyChanged,而不是从基继承,并在设置值后引发PropertyChanged时,它就起作用了。

这里的情况是WithEvents是.NET Framework本身不支持的一个功能。VB.NET是在.NET之上实现它的。之所以有这个功能,是因为它也是由VB6提供的。不过,在VB6中实现该功能的方式非常不同,因为COM和.NET.之间的事件模型存在根本差异

我不会详细介绍VB6是如何实现该功能的;这其实并不相关。重要的是事件如何与.NET协同工作。基本上,与.NET一样,事件必须显式挂钩和取消挂钩。定义事件时,与定义属性的方式有很多相似之处。特别是,有一种方法向事件添加处理程序,还有一种方法删除处理程序,类似于属性的"set"one_answers"get"方法之间的对称性。

事件之所以使用这样的方法,是为了向外部调用方隐藏附加处理程序的列表。如果类之外的代码可以访问附加处理程序的完整列表,那么它可能会干扰它,这将是一种非常糟糕的编程实践,可能会导致非常混乱的行为。

VB.NET通过AddHandlerRemoveHandler运算符公开了对这些事件"add"one_answers"remove"方法的直接调用。在C#中,使用+=-=运算符来表达完全相同的底层操作,其中左侧参数是事件成员引用。

WithEvents为您提供的是隐藏AddHandlerRemoveHandler调用的语法糖。重要的是要认识到,调用仍然存在,它们只是隐含的。

所以,当你写这样的代码时:

Private WithEvents _obj As ClassWithEvents
Private Sub _obj_GronkulatedEvent() Handles _obj.GronkulatedEvent
...
End Sub

您要求VB.NET确保无论哪个对象被分配给_obj(请记住,您可以随时更改该对象引用),事件GronkulatedEvent都应由该Sub处理。如果更改引用,则应立即分离旧对象的GronkulatedEvent,并附加新对象的CCD25。

VB.NET通过将字段转换为属性来实现这一点。添加WithEvents意味着字段_obj(或者,在您的情况下,_item)实际上不是字段。创建了一个秘密支持字段,然后_item成为一个属性,其实现如下所示:

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

那么,为什么这会导致你所看到的"滞后"呢?好吧,你不能传递属性"ByRef"。要传递"ByRef",您需要知道它的内存地址,但属性将内存地址隐藏在"get"one_answers"set"方法后面。在像C#这样的语言中,你只会得到一个编译时错误:属性不是L值,所以你不能传递对它的引用。然而,VB.NET更宽容,它会在幕后编写额外的代码,让事情为你工作。

在您的代码中,您要将看起来像的字段_item成员传递到SetProperty中,CCD_31接受参数ByRef,以便它可以写入新值。但是,由于WithEvents_item成员实际上是一个属性。那么,VB.NET是做什么的呢?它为对SetProperty的调用创建一个临时本地变量,然后在调用后将其分配回属性:

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

因此,由于WithEvents将字段转换为属性,VB.NET不得不将对该属性的实际赋值推迟到对SetProperty的调用返回之后。

希望这有意义!:-)

最新更新