代码之家  ›  专栏  ›  技术社区  ›  Brandon Hood

对WithEvents支持字段的延迟赋值

  •  1
  • Brandon Hood  · 技术社区  · 7 年前

    我注意到,当属性的支持字段具有 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即被命中,如预期的那样。但是,支持字段的值仍然是旧值!如果我再加一个 PropertyChanged 事件之后 集合属性 调用时,支持字段将在该点具有正确的值。当然,如果我把 以事件方式 ,它只在一个事件中按预期工作。

    这是我见过的唯一一次 集合属性 以这种方式失败。问题是什么 以事件方式 是什么原因?

    更新:何时 ViewModel 工具 INotifyPropertyChanged 直接,而不是从基继承,并引发 财产变更 设置值后,它会工作。

    1 回复  |  直到 7 年前
        1
  •  4
  •   Jonathan Gilbert    7 年前

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

    我不会详细介绍VB6是如何实现该功能的;这不是真的相关。重要的是事件如何与.NET一起工作。基本上,在.NET中,事件必须显式挂接和取消挂接。定义事件时,属性的定义方式有很多相似之处。特别是,有一个方法将处理程序添加到事件,还有一个方法删除处理程序,类似于属性的“set”和“get”方法之间的对称性。

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

    VB.NET通过 AddHandler RemoveHandler 操作员。在C#中,使用 += -= 运算符,其中左侧参数是事件成员引用。

    什么 以事件方式 给你的是隐藏 AddHandler 移除处理器 电话。重要的是要认识到 还在吗 而且,它们只是隐含的。

    因此,当您编写这样的代码时:

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

    …您要求VB.NET确保: 分配给的任何对象 _obj (请记住,您可以随时更改该对象引用),事件 GronkulatedEvent 应该这样处理 Sub .如果更改引用,则旧对象的 怪诞事件 应立即分离,并且新对象的 怪诞事件 附属的。

    VB.NET通过将字段转换为属性来实现这一点。添加 以事件方式 意味着这个领域 _目标 (或者,在您的情况下, _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”和“set”方法后面。在C#这样的语言中,您只会得到一个编译时错误:属性不是L值,因此无法传递对它的引用。然而,VB.NET更宽容,在幕后编写额外的代码,使事情为您服务。

    在您的代码中,您传递的是 看起来像 一个领域 _项目 成员,进入 SetProperty ,它接受参数 ByRef 所以它可以写一个新值。但是,由于 以事件方式 这个 _项目 成员实际上是一种财产。那么,VB.NET做什么呢?它为调用创建一个临时局部变量 集合属性 ,然后在调用后将其分配回属性:

    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
    

    所以,因为 以事件方式 将字段转换为属性,VB.NET必须将对属性的实际赋值推迟到调用 集合属性 返回。

    希望这有道理!:-)