【问题标题】:Property vs. Variable as ByRef parameter属性与变量作为 ByRef 参数
【发布时间】:2017-04-12 08:18:12
【问题描述】:

我创建了一个实现INotifyPropertyChanged 接口的基类。该类还包含一个通用函数SetProperty,用于设置任何属性的值并在必要时引发PropertyChanged 事件。

Public Class BaseClass
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Protected Function SetProperty(Of T)(ByRef storage As T, value As T, <CallerMemberName> Optional ByVal propertyName As String = Nothing) As Boolean
        If Object.Equals(storage, value) Then
            Return False
        End If

        storage = value
        Me.OnPropertyChanged(propertyName)
        Return True
    End Function

    Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional ByVal propertyName As String = Nothing)
        If String.IsNullOrEmpty(propertyName) Then
            Throw New ArgumentNullException(NameOf(propertyName))
        End If

        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub

End Class

然后我有一个类,它应该保存一些数据。为简单起见,它仅包含一个属性(在此示例中)。

Public Class Item
    Public Property Text As String
End Class

然后我有第三个类,它继承自基类并使用数据保存类。这第三个类应该是 WPF 窗口的 ViewModel。

我没有列出RelayCommand 类的代码,因为你们可能都有自己的实现。请记住,这个类在执行命令时执行给定的函数。

Public Class ViewModel
    Inherits BaseClass

    Private _text1 As Item   'data holding class
    Private _text2 As String   'simple variable
    Private _testCommand As ICommand = New RelayCommand(AddressOf Me.Test)

    Public Sub New()
        _text1 = New Item
    End Sub

    Public Property Text1 As String
        Get
            Return _text1.Text
        End Get
        Set(ByVal value As String)
            Me.SetProperty(Of String)(_text1.Text, value)
        End Set
    End Property

    Public Property Text2 As String
        Get
            Return _text2
        End Get
        Set(ByVal value As String)
            Me.SetProperty(Of String)(_text2, value)
        End Set
    End Property

    Public ReadOnly Property TestCommand As ICommand
        Get
            Return _testCommand
        End Get
    End Property

    Private Sub Test()
        Me.Text1 = "Text1"
        Me.Text2 = "Text2"
    End Sub

End Class

然后我的 WPF 窗口使用 ViewModel 类作为其DataContext。

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>

    <StackPanel Orientation="Horizontal">
        <TextBox Text="{Binding Text1}" Height="24" Width="100" />
        <TextBox Text="{Binding Text2}" Height="24" Width="100" />
        <Button Height="24" Content="Fill" Command="{Binding TestCommand}" />
    </StackPanel>
</Window>

如您所见,此窗口仅包含两个文本框和一个按钮。文本框绑定到属性Text1 和Text2,并且按钮应该执行命令TestCommand。

当命令执行时,Text1 和 Text2 两个属性都会被赋予一个值。由于这两个属性都会引发PropertyChanged 事件,因此这些值应该会显示在我的窗口中。

但我的窗口中只显示值“Text2”。

属性Text1 的值为“Text1”,但似乎该属性的PropertyChanged 事件在该属性获得其值之前引发。

有什么方法可以在我的基类中更改SetProperty 函数以在属性获得其值后提升PropertyChanged?

感谢您的帮助。

【问题讨论】:

标签: wpf vb.net inotifypropertychanged byref


【解决方案1】:

实际发生了什么?

这不起作用,因为属性的行为不像字段。

当您执行Me.SetProperty(Of String)(_text2, value) 时,会发生对字段_text2 的引用而不是其值,因此SetProperty 函数可以修改引用中的内容,并修改字段。

但是,当您执行Me.SetProperty(Of String)(_text1.Text, value) 时,编译器会看到属性的getter,因此它会首先调用_text1 的Get 属性,然后将返回值的引用作为参数传递。因此,当您的函数 SetProperty 接收 ByRef 参数时,它是 getter 的返回值,而不是实际的字段值。

据我了解here,如果你说你的属性是 ByRef,当你退出函数调用时编译器会自动更改字段 ref...所以这可以解释为什么它在你的事件之后发生变化...

This other blog 似乎证实了这种奇怪的行为。

【讨论】:

  • 但是属性的值会发生变化,但是只有在函数SetProperty离开的时候才会发生。
  • 那篇文章很好找。官方与否,它准确地描述了我看到的行为,如果我给 Item.Text 显式 getter/setter 和 Trace.WriteLine() 在每个。
【解决方案2】:

在 C# 中,等效代码无法编译。 .NET 不习惯通过引用传递属性,因为像 Eric Lippert 这样的人已经进入其他地方(我隐约记得 Eric 在 SO 上的某个地方针对 C# 解决了这个问题,但现在找不到 - 松散地说,它需要一种或另一种奇怪的解决方法,所有这些都有 C# 团队认为不可接受的缺点)。

VB 做到了,但作为一个相当奇怪的特殊情况:如果它创建一个通过引用传递的临时变量,然后在方法完成。这是一种解决方法(由 Eric Lippert 本人在下面的 cmets 中确认,另请参见 @Martin Verjans 的出色回答),对于不知道如何在 .NET 中实现 byref/ref 的任何人来说,其副作用都是违反直觉的。

仔细想想,他们不能让它正常工作,因为 VB.NET 和 C#(以及 F#,和 IronPython 等)必须相互兼容,所以一个VB ByRef 参数必须与从 C# 代码传入的 C# ref 参数兼容。因此,任何解决方法都必须完全由调用者负责。在理智的范围内,这将它限制在调用开始之前和返回之后可以做的事情。

ECMA 335 (Common Language Infrastructure) standard 的内容如下(Ctrl+F 搜索“byref”):

  • §I.8.2.1.1 托管指针和相关类型

    托管指针 (§I.12.1.1.2) 或byref (§I.8.6.1.3, §I.12.4.1.5.2) 可以指向复合类型的局部变量、参数、字段或数组元素。 ...

换句话说,就编译器而言,ByRef storage As T 实际上是内存中代码放置值的存储位置的地址。它在运行时非常有效,但没有为 getter 和 setter 提供语法糖魔法的范围。一个属性是一对方法,一个getter和一个setter(当然,或者只是一个或另一个)。

所以正如你所描述的,storage 在SetProperty() 中获取新值,在SetProperty() 完成后,_text1.Text 具有新值。但是编译器引入了一些神秘的恶作剧,导致事件的实际顺序不是你所期望的。

因此,SetProperty 不能以您编写的方式在 Text1 中使用。我测试过的最简单的解决方法是直接在Text1 的设置器中调用OnPropertyChanged()。

Public Property Text1 As String
    Get
        Return _text1.Text
    End Get
    Set(ByVal value As String)
        _text1.Text = value
        Me.OnPropertyChanged()
    End Set
End Property

没有办法处理这至少有点难看。你可以给Text1 一个像Text2 这样的常规支持字段,但是你需要保持它与_text1.Text 同步。这比上面的 IMO 更难看,因为您必须使两者保持同步,并且在 Text1 setter 中仍然有额外的代码。

【讨论】:

  • 我不太确定在 VB.NET 中是否不可能传递属性。我刚刚测试了给定的场景,并且在作为参考传递时属性的值发生了变化。
  • @Streamline 谢谢,我现在正在写一些测试代码。
  • @Streamline 你是对的,它确实改变了值——但只有在 SetProperty 退出之后,正如 OP 所描述的那样。很奇怪。
  • 在我的回答中,我找到了一个可以解释它的论坛链接。我试图弄清楚更多,但我还找不到任何官方来源......
  • 确实,问题在于底层CLR机制需要ref参数来获取变量,但属性实际上是一对方法我>。正如您所注意到的,在 VB 中,您可以通过 ref 传递属性;编译器生成一个变量,将属性读取到变量中,将 ref 传递给变量,从变量中读取值并将其写入属性。在 C# 中,如果你想这样,你必须自己编写代码。
猜你喜欢
  • 1970-01-01
  • 2019-12-21
  • 1970-01-01
  • 2013-01-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多