Skip to content

WithPropertyEvents modifier to enable easy INotifyPropertyChanged implementations #194

Open
@AnthonyDGreen

Description

@AnthonyDGreen

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:

  1. We're hesitant to bake a specific pattern into the language, as patterns can and do change.
  2. 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) is ByRef 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 a Try/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 or OnMethodInvoked and OnMethodReturn 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 the Get or Set method for that property.
  • This doesn't solve the repetitiveness of declaring Dependency properties. I'm OK with that.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions