属性与变量为ByRef参数

时间:2016-11-28 14:53:41

标签: wpf vb.net inotifypropertychanged byref

我创建了一个实现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>

如您所见,此窗口仅包含两个TextBox和一个按钮。 TextBoxes绑定到属性Text1Text2,按钮应该执行命令TestCommand

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

但只有价值&#34; Text2&#34;在我的窗口中显示。

属性Text1的值是&#34; Text1&#34;,但似乎在属性获取其值之前引发了此属性的PropertyChanged事件。

有没有办法在我的基类中更改SetProperty函数,以便在属性获得值后提升PropertyChanged

感谢您的帮助。

2 个答案:

答案 0 :(得分:4)

实际发生了什么?

这不起作用,因为属性不像字段那样。

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

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

根据我的理解here,如果您说您的属性是ByRef,编译器将在您退出函数调用时自动更改字段引用...这样就可以解释为什么它在事件发生后会发生变化。 ..

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

答案 1 :(得分:3)

在C#中,等效代码无法编译。 .NET通过引用传递属性并不方便,原因就像Eric Lippert这样的人已经进入其他地方(我模糊地回忆起Eric在某个地方看到了C#,但现在找不到它 - 松散地说,它需要一个奇怪的解决方法或另一个,所有这些都有C#团队认为不可接受的缺点)。

VB是这样做的,但作为一个相当奇怪的特殊情况:我所看到的行为是我所期望的,如果它创建一个临时变量,通过引用传递,然后然后将其值分配给属性方法完成。这是一种解决方法(由Eric Lippert在下面的评论中确认,另见@Martin Verjans的优秀答案)副作用与任何不知道如何实施byref / ref的人相反在.NET中。

当你考虑它时,它们不能使它正常工作,因为VB.NET和C#(和F#,和IronPython等等)必须相互兼容,所以a 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(当然只是一个或另一个)。

正如您所描述的那样,storageSetProperty()中获取新值,在SetProperty()完成后,_text1.Text具有新值。但编译器引入了一些隐匿的恶作剧,导致事件的实际序列不符合您的预期。

因此,SetProperty无法以Text1的方式使用OnPropertyChanged()。我测试过的最简单的修复方法是直接在Text1的setter中调用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常规支持字段,但是您需要将其与Text1保持同步。这比上面的IMO更丑陋,因为你必须保持两者同步,并且你仍然在HADOOP_CONF_DIR setter中有额外的代码。