延迟分配给 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 通过 AddHandlerRemoveHandler 运算符公开对这些事件 "add" 和 "remove" 方法的直接调用。在 C# 中,使用 +=-= 运算符表示完全相同的底层操作,其中 left-hand 参数是事件成员引用。

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

因此,当您编写如下代码时:

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.

希望这是有道理的! :-)