Property vs. Variable as ByRef parameter
Asked Answered
T

2

6

I created a base class that implements the INotifyPropertyChanged interface. This class also contains a generic function SetProperty to set the value of any property and raise the PropertyChanged event, if necessary.

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

Then I have a class, that is supposed to hold some data. For the sake of simplicity it only contains one property (in this example).

Public Class Item
    Public Property Text As String
End Class

Then I have a third class that inherits from the base class and uses the data holding class. This third class is supposed to be a ViewModel for a WPF window.

I don't list the code for the RelayCommand class, since you probably all have an implementation yourself. Just keep in mind, that this class executes the given function, when the command is executed.

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

And then I have my WPF window that uses the ViewModel class as its 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>

As you can see, this window contains only two TextBoxes and a button. The TextBoxes are bound to the properties Text1 and Text2 and the button is supposed to execute the command TestCommand.

When the command is executed both properties Text1 and Text2 is given a value. And since both properties raise the PropertyChanged event, these values should be shown in my window.

But only the value "Text2" is shown in my window.

The value of property Text1 is "Text1", but it seems that the PropertyChanged event for this property is raised before the property got its value.

Is there any way to change the SetProperty function in my base class to raise the PropertyChanged after the property got its value?

Thank you for your help.

Turnaround answered 28/11, 2016 at 14:53 Comment(1)
A
4

What actually happens ?

This doesn't work because the properties don't behave as fields do.

When you do Me.SetProperty(Of String)(_text2, value), what happens is that the reference to the field _text2 is passed instead of its value, so the SetProperty function can modify what's inside the reference, and the field is modified.

However, when you do Me.SetProperty(Of String)(_text1.Text, value), the compiler sees a getter for a property, so it will first call the Get property of _text1, then pass the reference to the return value as parameter. So when your function SetProperty is receving the ByRef parameter, it is the return value from the getter, and not the actual field value.

From what I understood here, if you say that your property is ByRef, the compiler will automatically change the field ref when you exit the function call... So that would explain why it's changing after your event...

This other blog seems to confirm this strange behavior.

Adduce answered 28/11, 2016 at 15:29 Comment(2)
But the value of the property changes, but only when the function SetProperty is left.Turnaround
Good find with that article. Official or not, it describes exactly the behavior I'm seeing if I give Item.Text explicit getter/setter with Trace.WriteLine() in each.Tangerine
T
4

In C#, the equivalent code wouldn't compile. .NET isn't comfortable passing properties by reference, for reasons which folks like Eric Lippert have gone into elsewhere (I dimly recall Eric addressing the matter vis a vis C# somewhere on SO, but can't find it now -- loosely speaking, it would require one weird workaround or another, all of which have shortcomings that the C# team regards as unacceptable).

VB does it, but as a rather strange special case: The behavior I'm seeing is what I would expect if it were creating a temporary variable which is passed by reference, and then then assigning its value to the property after the method completes. This is a workaround (confirmed by Eric Lippert himself below in comments, see also @Martin Verjans' excellent answer) with side effects that are counterintuitive for anybody who doesn't know how byref/ref are implemented in .NET.

When you think about it, they can't make it work properly, because VB.NET and C# (and F#, and IronPython, etc. etc.) must be mutually compatible, so a VB ByRef parameter must be compatible with a C# ref argument passed in from C# code. Therefore, any workaround has to be entirely the caller's responsibility. Within the bounds of sanity, that limits it to what it can do before the call begins, and after it returns.

Here's what the ECMA 335 (Common Language Infrastructure) standard has to say (Ctrl+F search for "byref"):

  • §I.8.2.1.1   Managed pointers and related types

    A managed pointer (§I.12.1.1.2), or byref (§I.8.6.1.3, §I.12.4.1.5.2), can point to a local variable, parameter, field of a compound type, or element of an array. ...

In other words, as far as the compiler is concerned, ByRef storage As T is actually the address of a storage location in memory where the code puts a value. It's very efficient at runtime, but offers no scope for syntactic sugar magic with getters and setters. A property is a pair of methods, a getter and a setter (or just one or the other, of course).

So as you describe, storage gets the new value inside SetProperty(), and after SetProperty() completes, _text1.Text has the new value. But the compiler has introduced some occult shenanigans which cause the actual sequence of events not to be what you expect.

As a result, SetProperty cannot be used in Text1 the way you wrote it. The simplest fix, which I have tested, is to call OnPropertyChanged() directly in the setter for Text1.

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

There's no way to handle this that isn't at least a little bit ugly. You could give Text1 a regular backing field like Text2 has, but then you'd need to keep that in sync with _text1.Text. That's uglier than the above IMO because you have to keep the two in sync, and you still have extra code in the Text1 setter.

Tangerine answered 28/11, 2016 at 15:17 Comment(5)
I am not quite sure if passing a property is impossible in VB.NET. I just tested the given scenario and the value of the property is changed when passed as reference.Triforium
@Triforium Thanks, I'm writing some test code now.Tangerine
@Triforium You're right, it does change the value -- but only after SetProperty exits, exactly as OP describes. Very strange.Tangerine
In my answer, I found a link to a forum that kind of explains it. I have tried to figure out more but I couldn't find any official source yet...Adduce
Indeed, the problem is that ref parameters are required by the underlying CLR mechanisms to get a variable but a property is actually a pair of methods. As you note, in VB you can pass a property by ref; the compiler generates a variable, reads the property into the variable, passes a ref to the variable, reads the value out of the variable and writes it to the property. In C# if that's what you want to happen you have to write the code yourself.Nonsmoker
A
4

What actually happens ?

This doesn't work because the properties don't behave as fields do.

When you do Me.SetProperty(Of String)(_text2, value), what happens is that the reference to the field _text2 is passed instead of its value, so the SetProperty function can modify what's inside the reference, and the field is modified.

However, when you do Me.SetProperty(Of String)(_text1.Text, value), the compiler sees a getter for a property, so it will first call the Get property of _text1, then pass the reference to the return value as parameter. So when your function SetProperty is receving the ByRef parameter, it is the return value from the getter, and not the actual field value.

From what I understood here, if you say that your property is ByRef, the compiler will automatically change the field ref when you exit the function call... So that would explain why it's changing after your event...

This other blog seems to confirm this strange behavior.

Adduce answered 28/11, 2016 at 15:29 Comment(2)
But the value of the property changes, but only when the function SetProperty is left.Turnaround
Good find with that article. Official or not, it describes exactly the behavior I'm seeing if I give Item.Text explicit getter/setter with Trace.WriteLine() in each.Tangerine

© 2022 - 2024 — McMap. All rights reserved.