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.
Passing ByRef or ByVal indicates whether the actual value of an argument is passed to the CalledProcedure by the CallingProcedure, or whether a reference (called a pointer in some other languages) is passed to to the CalledProcedure.
The advantage of passing an argument ByRef is that the procedure can return a value to the calling code through that argument. The advantage of passing an argument ByVal is that it protects a variable from being changed by the procedure.
ByRef in means that a reference to the original value will be sent to the function. It's almost like the original value is being directly used within the function. Operations like = will affect the original value and be immediately visible in the calling function .
The default is ByVal , but be sure you understand what passing by value and by reference actually means.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With