Description
Scenario
INotifyPropertyChanged
is essential for modern UI patterns like MVVM. Today developers must add code to all of their properties to take advantage of the interface and that is repetitive and erodes the value of automatically implemented properties. Here is a typical pattern:
Property Name As String
Get
Return _Name
End Get
Set(value As String)
If value = _Name Then Return
_Name = value
OnPropertyChanged()
End Set
End Property
Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = "")
RaiseEvent PropertyChanged(New PropertyChangedEventArgs(propertyName))
End Sub
Customers have been begging us for years for a solution to this problem but we've been reluctant for several reasons:
- We're hesitant to bake a specific pattern into the language, as patterns can and do change.
- We prefer general solutions to specific solutions and have been pursuing a general meta-programming feature that would encompass the
INotifyPropertyChanged
problem.
But, in the last VB LDM, VB MVP @KlausLoeffelmann was on campus as our guest and convinced us to get over it, so I'm pulling this proposal out of moth-balls.
Proposal
I proposal a new class modifier, tentatively WithEvents
or WithPropertyEvents
. When this modifier is present the code generated for auto-implemented properties will invoke a set of pseudo-partial methods. They're not true partial methods for several reasons, but are like partial methods in one particular way--calls to them are only emitted if they have been defined in user code. If user code implements these properties they'll be called. Users can easily leverage this to implement INotifyPropertyChanged
without having to use expanded properties. It'll work something like this:
WithEvents Class Person
Property Name As String
End Class
Will translate to:
WithEvents Class Person
<CompilerGenerated>
<Hidden>
Private _Name As String
Property Name As String
Get
OnPropertyGet(NameOf(Name), _Name)
Return _Name
End Get
Set(value As String)
Dim oldValue = _Name
If Not OnPropertySetting(NameOf(Name), oldValue, value) Then Return
_Name = value
OnPropertySet(NameOf(Name), oldValue, value)
End Set
End Property
End Class
Essentially auto-props will now have well-known places where users can plug in other things. Normal overload resolution will be performed on the found methods so if the user wants to make things generic or strongly-typed they can.
The idea behind the OnPropertySetting
call is two fold. It's very common for properties to disregard redundant sets to their current value. The implementer of OnPropertySetting
can do several things:
- By returning
False
they could early exit the entire property. - If the 3rd parameter (
value
) isByRef
they can change the value being set, e.g. (trim a string). - Raise
INotifyPropertyChanging
. - Log a change before it happens.
- Throw an exception if something is wrong.
In practice code leveraging this feature would look like this.
' Write once
Public Class ViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Overridable Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = "")
RaiseEvent PropertyChanged(New PropertyChangedEventArgs(propertyName))
End Sub
Protected Overridable Function OnPropertySetting(Of T)(propertyName As String, oldValue As T, newValue As T) As Boolean
Return Not Object.Equals(oldValue, newValue)
End Function
Protected Overridable Sub OnPropertySet(Of T)(propertyName As String, oldValue As T, newValue As T)
OnPropertyChanged(propertyName)
End Function
End Class
' In this new bright future most VB users would only need to write this class because the feature just looks up the method normally and all the plumbing can be implemented once in a base class.
WithEvents Class Person
Inherits ViewModel
Property FirstName As String
Property MiddleName As String
Property LastName As String
Property DateOfBirth As Date
End Class
Undoubtedly there will be a case where someone wants to opt-out of this functionality. I propose that another new member modifier, NoEvents
, can be specified on a property to opt it out of the default behavior.
Discussion points
- How do we avoid breaking changes? The feature requires opt-in through some syntax.
- Should it be a class modifier? Yes, specifying the modifier on every property would be annoying.
- If it's a class modifier, what should the keyword be? <Let's discuss>
- Does this work for structures? No reason to disallow.
- Does this work for
Shared
properties? No reason to disallow. - Does this work for modules? No reason to disallow.
- Could we support other property "events" like:
OnPropertySetFault
--called when an auto-prop throws an exception. Its existence causes the body of the auto-prop to be wrapped in aTry
/Catch
/Finally
block.
- Could we support other members like auto-implemented events? No scenario for this.
- Could we generalize this further to support callbacks in any member body in a class, e.g.
WithMethodEvents
orOnMethodInvoked
andOnMethodReturn
to support logging or other instrumentation? <Let's discuss> - Should add another layer of precedence to this feature by first looking for a specifically named member to call, e.g.
OnFirstNamePropertySetting
,OnFirstNamePropertySet
, etc. and only calling the general method if a more specific method isn't implemented? This would allow for some special handling of one-offs, though arguable such functionality would best be written in theGet
orSet
method for that property. - This doesn't solve the repetitiveness of declaring Dependency properties. I'm OK with that.