延迟分配给 WithEvents 支持字段
Delayed assignment to a WithEvents backing field
我注意到当 属性 的支持字段具有 WithEvents
修饰符时,赋值可以 "lag" 因为缺少更好的词。我已经在一个简单的演示中重现了该行为,因此 WithEvents
的目的在这里并不明显(因此说 "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
定义:
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,因为预期的。但是,backing 字段的值仍然是旧值!如果我在 SetProperty
调用之后立即添加另一个 PropertyChanged
事件,则支持字段此时将具有正确的值。当然,如果我取出 WithEvents
,它只需要一个事件就可以正常工作。
这是我唯一一次看到 SetProperty
以这种方式失败。 WithEvents
导致的问题是什么?
更新:当 ViewModel
直接实现 INotifyPropertyChanged
,而不是从基础继承,并在设置值后引发 PropertyChanged
时,它起作用了。
这里发生的事情是 WithEvents
是 .NET Framework 本身不支持的功能。 VB.NET 正在 .NET 之上实施它。该功能之所以存在,是因为它也是由 VB6 提供的。但是,该功能在 VB6 中的实现方式非常不同,因为 COM 和 .NET 之间的事件模型存在根本差异。
我不会深入探讨 VB6 如何实现该功能;那不是真的相关。重要的是事件如何与 .NET 一起工作。基本上,对于 .NET,事件必须显式挂钩和取消挂钩。定义事件时,与定义属性的方式有很多相似之处。特别是,有一种方法可以将处理程序添加到事件,也可以使用一种方法来删除处理程序,类似于 属性 具有的 "set" 和 "get" 方法之间的对称性。
事件使用这样的方法的原因是为了对外部调用者隐藏附加处理程序的列表。如果 class 之外的代码可以访问附加处理程序的完整列表,它可能会干扰它,这将是一种非常糟糕的编程实践,可能会导致非常混乱的行为。
VB.NET 通过 AddHandler
和 RemoveHandler
运算符公开对这些事件 "add" 和 "remove" 方法的直接调用。在 C# 中,使用 +=
和 -=
运算符表示完全相同的底层操作,其中 left-hand 参数是事件成员引用。
WithEvents
为您提供的是隐藏 AddHandler
和 RemoveHandler
调用的语法糖。重要的是要认识到调用 仍然存在 ,它们只是隐含的。
因此,当您编写如下代码时:
Private WithEvents _obj As ClassWithEvents
Private Sub _obj_GronkulatedEvent() Handles _obj.GronkulatedEvent
...
End Sub
..您要求 VB.NET 确保 分配给 _obj
的任何对象(请记住,您可以更改该对象引用任何时候),事件 GronkulatedEvent
应该由那个 Sub
处理。如果更改引用,则应立即分离旧对象的 GronkulatedEvent
,并附加新对象的 GronkulatedEvent
。
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
那么,为什么这会导致您看到 "lag"?好吧,你不能传递 属性 "ByRef"。要传递某些东西 "ByRef",您需要知道它的内存地址,但是 属性 将内存地址隐藏在 "get" 和 "set" 方法之后。在像 C# 这样的语言中,您只会收到 compile-time 错误:A 属性 不是 L-value,因此您无法传递对它的引用。但是,VB.NET 更宽容,会在幕后编写额外的代码来让事情为您服务。
在您的代码中,您正在将 看起来像 的字段(_item
成员)传递给 SetProperty
,它采用参数 [=32] =] 所以它可以写一个新值。但是,由于 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 不得不推迟对 属性 的实际赋值,直到调用 [=31] =] returns.
希望这是有道理的! :-)
我注意到当 属性 的支持字段具有 WithEvents
修饰符时,赋值可以 "lag" 因为缺少更好的词。我已经在一个简单的演示中重现了该行为,因此 WithEvents
的目的在这里并不明显(因此说 "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
定义:
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,因为预期的。但是,backing 字段的值仍然是旧值!如果我在 SetProperty
调用之后立即添加另一个 PropertyChanged
事件,则支持字段此时将具有正确的值。当然,如果我取出 WithEvents
,它只需要一个事件就可以正常工作。
这是我唯一一次看到 SetProperty
以这种方式失败。 WithEvents
导致的问题是什么?
更新:当 ViewModel
直接实现 INotifyPropertyChanged
,而不是从基础继承,并在设置值后引发 PropertyChanged
时,它起作用了。
这里发生的事情是 WithEvents
是 .NET Framework 本身不支持的功能。 VB.NET 正在 .NET 之上实施它。该功能之所以存在,是因为它也是由 VB6 提供的。但是,该功能在 VB6 中的实现方式非常不同,因为 COM 和 .NET 之间的事件模型存在根本差异。
我不会深入探讨 VB6 如何实现该功能;那不是真的相关。重要的是事件如何与 .NET 一起工作。基本上,对于 .NET,事件必须显式挂钩和取消挂钩。定义事件时,与定义属性的方式有很多相似之处。特别是,有一种方法可以将处理程序添加到事件,也可以使用一种方法来删除处理程序,类似于 属性 具有的 "set" 和 "get" 方法之间的对称性。
事件使用这样的方法的原因是为了对外部调用者隐藏附加处理程序的列表。如果 class 之外的代码可以访问附加处理程序的完整列表,它可能会干扰它,这将是一种非常糟糕的编程实践,可能会导致非常混乱的行为。
VB.NET 通过 AddHandler
和 RemoveHandler
运算符公开对这些事件 "add" 和 "remove" 方法的直接调用。在 C# 中,使用 +=
和 -=
运算符表示完全相同的底层操作,其中 left-hand 参数是事件成员引用。
WithEvents
为您提供的是隐藏 AddHandler
和 RemoveHandler
调用的语法糖。重要的是要认识到调用 仍然存在 ,它们只是隐含的。
因此,当您编写如下代码时:
Private WithEvents _obj As ClassWithEvents
Private Sub _obj_GronkulatedEvent() Handles _obj.GronkulatedEvent
...
End Sub
..您要求 VB.NET 确保 分配给 _obj
的任何对象(请记住,您可以更改该对象引用任何时候),事件 GronkulatedEvent
应该由那个 Sub
处理。如果更改引用,则应立即分离旧对象的 GronkulatedEvent
,并附加新对象的 GronkulatedEvent
。
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
那么,为什么这会导致您看到 "lag"?好吧,你不能传递 属性 "ByRef"。要传递某些东西 "ByRef",您需要知道它的内存地址,但是 属性 将内存地址隐藏在 "get" 和 "set" 方法之后。在像 C# 这样的语言中,您只会收到 compile-time 错误:A 属性 不是 L-value,因此您无法传递对它的引用。但是,VB.NET 更宽容,会在幕后编写额外的代码来让事情为您服务。
在您的代码中,您正在将 看起来像 的字段(_item
成员)传递给 SetProperty
,它采用参数 [=32] =] 所以它可以写一个新值。但是,由于 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 不得不推迟对 属性 的实际赋值,直到调用 [=31] =] returns.
希望这是有道理的! :-)