10
\$\begingroup\$

I've written an API that unlocks some serious MVVM capabilities with VBA's MSForms UI framework.

This is the complete code-behind for an example UserForm "view" - it's not the code under review, but since I'm 99% sure I won't have enough room to fit everything here I figure it gives a nice idea of what Model-View-ViewModel looks like in VBA - note that a more complex view implementation would only need more bindings, the only other code is just boilerplate to support cancellation:

'@Folder MVVM.Example
'@ModuleDescription "An example implementation of a View."
Implements IView
Implements ICancellable
Option Explicit

Private Type TView
    'IView state:
    ViewModel As ExampleViewModel
    
    'ICancellable state:
    IsCancelled As Boolean
    
    'Data binding helper dependency:
    Bindings As IBindingManager
End Type

Private this As TView

'@Description "A factory method to create new instances of this View, already wired-up to a ViewModel."
Public Function Create(ByVal ViewModel As ExampleViewModel, ByVal Bindings As IBindingManager) As IView
    GuardClauses.GuardNonDefaultInstance Me, ExampleView, TypeName(Me)
    GuardClauses.GuardNullReference ViewModel, TypeName(Me)
    GuardClauses.GuardNullReference Bindings, TypeName(Me)
    
    Dim result As ExampleView
    Set result = New ExampleView
    
    Set result.Bindings = Bindings
    Set result.ViewModel = ViewModel
    
    Set Create = result
    
End Function

Private Property Get IsDefaultInstance() As Boolean
    IsDefaultInstance = Me Is ExampleView
End Property

'@Description "Gets/sets the ViewModel to use as a context for property and command bindings."
Public Property Get ViewModel() As ExampleViewModel
    Set ViewModel = this.ViewModel
End Property

Public Property Set ViewModel(ByVal RHS As ExampleViewModel)
    GuardClauses.GuardExpression IsDefaultInstance, TypeName(Me)
    GuardClauses.GuardNullReference RHS
    
    Set this.ViewModel = RHS
    InitializeBindings

End Property

'@Description "Gets/sets the binding manager implementation."
Public Property Get Bindings() As IBindingManager
    Set Bindings = this.Bindings
End Property

Public Property Set Bindings(ByVal RHS As IBindingManager)
    GuardClauses.GuardExpression IsDefaultInstance, TypeName(Me)
    GuardClauses.GuardDoubleInitialization this.Bindings, TypeName(Me)
    GuardClauses.GuardNullReference RHS
    
    Set this.Bindings = RHS

End Property

Private Sub BindViewModelCommands()
    With Bindings
        .BindCommand ViewModel, Me.OkButton, AcceptCommand.Create(Me)
        .BindCommand ViewModel, Me.CancelButton, CancelCommand.Create(Me)
        .BindCommand ViewModel, Me.BrowseButton, ViewModel.SomeCommand
        '...
    End With
End Sub

Private Sub BindViewModelProperties()
    With Bindings
        
        .BindPropertyPath ViewModel, "SourcePath", Me.PathBox, _
            Validator:=New RequiredStringValidator, _
            ErrorFormat:=AggregateErrorFormatter.Create(ViewModel, _
                ValidationErrorFormatter.Create(Me.PathBox).WithErrorBackgroundColor.WithErrorBorderColor, _
                ValidationErrorFormatter.Create(Me.InvalidPathIcon).WithTargetOnlyVisibleOnError("SourcePath"), _
                ValidationErrorFormatter.Create(Me.ValidationMessage1).WithTargetOnlyVisibleOnError("SourcePath"))
        
        .BindPropertyPath ViewModel, "Instructions", Me.InstructionsLabel
        
        .BindPropertyPath ViewModel, "SomeOption", Me.OptionButton1
        .BindPropertyPath ViewModel, "SomeOtherOption", Me.OptionButton2
        .BindPropertyPath ViewModel, "SomeOptionName", Me.OptionButton1, "Caption", OneTimeBinding
        .BindPropertyPath ViewModel, "SomeOtherOptionName", Me.OptionButton2, "Caption", OneTimeBinding
        
        '...
        
    End With
End Sub

Private Sub InitializeBindings()
    If ViewModel Is Nothing Then Exit Sub
    BindViewModelProperties
    BindViewModelCommands
    Bindings.ApplyBindings ViewModel
End Sub

Private Sub OnCancel()
    this.IsCancelled = True
    Me.Hide
End Sub

Private Property Get ICancellable_IsCancelled() As Boolean
    ICancellable_IsCancelled = this.IsCancelled
End Property

Private Sub ICancellable_OnCancel()
    OnCancel
End Sub

Private Sub IView_Hide()
    Me.Hide
End Sub

Private Sub IView_Show()
    Me.Show vbModal
End Sub

Private Function IView_ShowDialog() As Boolean
    Me.Show vbModal
    IView_ShowDialog = Not this.IsCancelled
End Function

Private Property Get IView_ViewModel() As Object
    Set IView_ViewModel = this.ViewModel
End Property

screenshot of the example View showing the bindings in action

The lorem ipsum title comes from the binding to the Instructions ViewModel property; the OK button is disabled because the command it's bound to cannot be executed.. because the ViewModel has more than 0 validation errors. The input field has an error icon, a pink background and a red border thanks to the AggregateErrorFormatter linked to the binding. If we selected other option button, the BrowseCommand would be executable and the associated button would have a tooltip - all without manually handling a single event.

Property Bindings

At the core of everything is the IPropertyBinding interface, defined as follows:

IPropertyBinding

'@Folder MVVM.Infrastructure.Bindings
'@ModuleDescription "An object responsible for binding a ViewModel property path to a UI element."
'@Interface
'@Exposed
Option Explicit

Public Enum BindingMode
    TwoWayBinding
    OneWayBinding
    OneWayToSource
    OneTimeBinding
End Enum

Public Enum BindingUpdateSourceTrigger
    OnPropertyChanged
    OnKeyPress
    OnExit
End Enum

'@Description "Gets a value indicating the binding mode/direction."
Public Property Get Mode() As BindingMode
End Property

'@Description "Gets a value indicating the binding update trigger."
Public Property Get UpdateSourceTrigger() As BindingUpdateSourceTrigger
End Property

'@Description "Gets the ViewModel object that is the binding source."
Public Property Get Source() As Object
End Property

'@Description "Gets a path to a Public Property Get procedure resolvable from the binding source."
Public Property Get SourcePropertyPath() As String
End Property

'@Description "Gets the UI element that is the binding target."
Public Property Get Target() As Object
End Property

'@Description "Gets the name of the bound property on the target object."
Public Property Get TargetProperty() As String
End Property

'@Description "Gets the converter to use (if any) when applying the binding."
Public Property Get Converter() As IValueConverter
End Property

'@Description "Applies the binding."
Public Sub Apply()
End Sub

The implementation is over 550 lines of code, mostly because it's being too many things at once, but I'm not sure how criminal it is to have the PropertyBinding class be self-contained like this. On the other hand all flags are raised and the bells are ringing, the interface needs separate implementations per control type, and suddenly it doesn't seem like a lot of code anymore. Worth splitting it up?

The class implements IHandlePropertyChanged and registers ViewModel INotifyPropertyChanged events in the Create factory method, like this:

    If Mode <> OneWayToSource And TypeOf Source Is INotifyPropertyChanged Then
        Dim notifier As INotifyPropertyChanged
        Set notifier = Source
        notifier.RegisterHandler result
    End If

The class also implements the INotifyPropertyChanged interface, and its callback is registered in the BindingManager class, in the BindPropertyPath method; the BindingManager handles property changes by re-evaluating the commands' CanExecute method, which completes binding loop and makes commands responsive to property changes, just like property bindings.

PropertyBinding

'@Folder MVVM.Infrastructure.Bindings
'@ModuleDescription "An object responsible for binding a ViewModel property path to a UI element."
'@PredeclaredId
'@Exposed
Implements IPropertyBinding
Implements IHandlePropertyChanged
Implements INotifyPropertyChanged
Option Explicit

Private Type TState
    Handlers As Collection
    Manager As IBindingManager
    Mode As BindingMode
    UpdateSourceTrigger As BindingUpdateSourceTrigger
    Source As Object
    SourcePropertyPath As String
    Target As Object
    TargetProperty As String
    Converter As IValueConverter
    Validator As IValueValidator
    ValidationErrorHandler As IHandleValidationError
    ValidationErrorFormatter As IValidationErrorFormatter
    Applied As Boolean
End Type

Private WithEvents ControlEventSource As MSForms.Control
Private WithEvents TextBoxEventSource As MSForms.TextBox
Private WithEvents CheckBoxEventSource As MSForms.CheckBox
Private WithEvents OptionButtonEventSource As MSForms.OptionButton
Private WithEvents ComboBoxEventSource As MSForms.ComboBox
Private WithEvents ListBoxEventSource As MSForms.ListBox

Private this As TState

Public Function ForTextBox(ByVal Manager As IBindingManager, ByVal Target As MSForms.TextBox, ByVal Source As Object, ByVal SourceProperty As String, _
Optional ByVal Mode As BindingMode = BindingMode.TwoWayBinding, _
Optional ByVal UpdateSource As BindingUpdateSourceTrigger = OnPropertyChanged, _
Optional ByVal Validator As IValueValidator, _
Optional ByVal Converter As IValueConverter, _
Optional ByVal ErrorFormat As IValidationErrorFormatter) As IPropertyBinding
    Set ForTextBox = Create(Manager, Target, "Text", Source, SourceProperty, Mode, UpdateSource, Validator, Converter, ErrorFormat)
End Function

Public Function ForCheckBox(ByVal Manager As IBindingManager, ByVal Target As MSForms.CheckBox, ByVal Source As Object, ByVal SourceProperty As String, _
Optional ByVal Mode As BindingMode = BindingMode.TwoWayBinding, _
Optional ByVal Validator As IValueValidator, _
Optional ByVal Converter As IValueConverter, _
Optional ByVal ErrorFormat As IValidationErrorFormatter) As IPropertyBinding
    Set ForCheckBox = Create(Manager, Target, "Value", Source, SourceProperty, Mode, OnPropertyChanged, Validator, Converter, ErrorFormat)
End Function

Public Function ForOptionButton(ByVal Manager As IBindingManager, ByVal Target As MSForms.OptionButton, ByVal Source As Object, ByVal SourceProperty As String, _
Optional ByVal Mode As BindingMode = BindingMode.TwoWayBinding, _
Optional ByVal Validator As IValueValidator, _
Optional ByVal Converter As IValueConverter, _
Optional ByVal ErrorFormat As IValidationErrorFormatter) As IPropertyBinding
    Set ForOptionButton = Create(Manager, Target, "Value", Source, SourceProperty, Mode, OnPropertyChanged, Validator, Converter, ErrorFormat)
End Function

Public Function ForLabel(ByVal Manager As IBindingManager, ByVal Target As MSForms.Label, ByVal Source As Object, ByVal SourceProperty As String, _
Optional ByVal Mode As BindingMode = BindingMode.OneWayBinding, _
Optional ByVal Validator As IValueValidator, _
Optional ByVal Converter As IValueConverter, _
Optional ByVal ErrorFormat As IValidationErrorFormatter) As IPropertyBinding
    Set ForLabel = Create(Manager, Target, "Caption", Source, SourceProperty, Mode, OnPropertyChanged, Validator, Converter, ErrorFormat)
End Function

Public Function ForFrame(ByVal Manager As IBindingManager, ByVal Target As MSForms.Frame, ByVal Source As Object, ByVal SourceProperty As String, _
Optional ByVal Mode As BindingMode = BindingMode.OneWayBinding, _
Optional ByVal Validator As IValueValidator, _
Optional ByVal Converter As IValueConverter, _
Optional ByVal ErrorFormat As IValidationErrorFormatter) As IPropertyBinding
    Set ForFrame = Create(Manager, Target, "Caption", Source, SourceProperty, Mode, OnPropertyChanged, Validator, Converter, ErrorFormat)
End Function

Public Function ForComboBoxValue(ByVal Manager As IBindingManager, ByVal Target As MSForms.ComboBox, ByVal Source As Object, ByVal SourceProperty As String, _
Optional ByVal Mode As BindingMode = BindingMode.TwoWayBinding, _
Optional ByVal Validator As IValueValidator, _
Optional ByVal Converter As IValueConverter, _
Optional ByVal ErrorFormat As IValidationErrorFormatter) As IPropertyBinding
    Set ForComboBoxValue = Create(Manager, Target, "Value", Source, SourceProperty, Mode, OnPropertyChanged, Validator, Converter, ErrorFormat)
End Function

Public Function ForListBoxValue(ByVal Manager As IBindingManager, ByVal Target As MSForms.ListBox, ByVal Source As Object, ByVal SourceProperty As String, Optional ByVal Mode As BindingMode = BindingMode.TwoWayBinding, Optional ByVal Validator As IValueValidator, Optional ByVal Converter As IValueConverter) As IPropertyBinding
    Set ForListBoxValue = Create(Manager, Target, "Value", Source, SourceProperty, Mode, OnPropertyChanged, Validator, Converter)
End Function

Public Function Create(ByVal Manager As IBindingManager, ByVal Target As Object, ByVal TargetProperty As String, ByVal Source As Object, ByVal SourceProperty As String, _
Optional ByVal Mode As BindingMode = BindingMode.TwoWayBinding, _
Optional ByVal UpdateSource As BindingUpdateSourceTrigger = BindingUpdateSourceTrigger.OnPropertyChanged, _
Optional ByVal Validator As IValueValidator, _
Optional ByVal Converter As IValueConverter, _
Optional ByVal ErrorFormat As IValidationErrorFormatter) As IPropertyBinding
    
    Dim result As PropertyBinding
    Set result = New PropertyBinding
    
    Set result.Target = Target
    result.TargetProperty = TargetProperty
    
    Set result.Source = Source
    result.SourcePropertyPath = SourceProperty
    
    Set result.Manager = Manager
    Set result.Converter = Converter
    Set result.Validator = Validator
    Set result.ValidationErrorFormatter = ErrorFormat
    result.Mode = Mode
    result.UpdateSourceTrigger = UpdateSource
    
    If Mode <> OneWayToSource And TypeOf Source Is INotifyPropertyChanged Then
        Dim notifier As INotifyPropertyChanged
        Set notifier = Source
        notifier.RegisterHandler result
    End If
    
    Set Create = result
    
End Function

Public Property Get Source() As Object
    Set Source = this.Source
End Property

Public Property Set Source(ByVal RHS As Object)
    GuardClauses.GuardDoubleInitialization this.Manager, TypeName(Me)
    GuardClauses.GuardNullReference RHS, TypeName(Me)
    Set this.Source = RHS
    If TypeOf RHS Is IViewModel Then
        Dim ViewModel As IViewModel
        Set ViewModel = RHS
        Set this.ValidationErrorHandler = ViewModel.Validation
    End If
End Property

Public Property Get Target() As Object
    Set Target = this.Target
End Property

Public Property Set Target(ByVal RHS As Object)
    GuardClauses.GuardDoubleInitialization this.Target, TypeName(Me)
    GuardClauses.GuardNullReference RHS, TypeName(Me)
    
    Set this.Target = RHS
    
    Set ControlEventSource = Nothing
    Set TextBoxEventSource = Nothing
    Set CheckBoxEventSource = Nothing
    Set ComboBoxEventSource = Nothing
    Set ListBoxEventSource = Nothing
    Set OptionButtonEventSource = Nothing
    
    If TypeOf this.Target Is MSForms.Control And this.UpdateSourceTrigger = OnExit Then Set ControlEventSource = this.Target
    
    Select Case True
    
        Case TypeOf this.Target Is MSForms.TextBox
            Set TextBoxEventSource = this.Target
            
        Case TypeOf this.Target Is MSForms.CheckBox And TypeName(this.Target) = "CheckBox" 'OptionButton matches the interface criteria
            Set CheckBoxEventSource = this.Target
            
        Case TypeOf this.Target Is MSForms.ComboBox
            Set ComboBoxEventSource = this.Target
            
        Case TypeOf this.Target Is MSForms.ListBox
            Set ListBoxEventSource = this.Target
            
        Case TypeOf this.Target Is MSForms.OptionButton
            Set OptionButtonEventSource = this.Target
            
    End Select
    
End Property

Public Property Get SourcePropertyPath() As String
    SourcePropertyPath = this.SourcePropertyPath
End Property

Public Property Let SourcePropertyPath(ByVal RHS As String)
    this.SourcePropertyPath = RHS
End Property

Public Property Get TargetProperty() As String
    TargetProperty = this.TargetProperty
End Property

Public Property Let TargetProperty(ByVal RHS As String)
    this.TargetProperty = RHS
End Property

Public Property Get Mode() As BindingMode
    Mode = this.Mode
End Property

Public Property Let Mode(ByVal RHS As BindingMode)
    this.Mode = RHS
End Property

Public Property Get UpdateSourceTrigger() As BindingUpdateSourceTrigger
    UpdateSourceTrigger = this.UpdateSourceTrigger
End Property

Public Property Let UpdateSourceTrigger(ByVal RHS As BindingUpdateSourceTrigger)
    this.UpdateSourceTrigger = RHS
End Property

Public Property Get Validator() As IValueValidator
    Set Validator = this.Validator
End Property

Public Property Set Validator(ByVal RHS As IValueValidator)
    Set this.Validator = RHS
End Property

Public Property Get ValidationErrorFormatter() As IValidationErrorFormatter
    Set ValidationErrorFormatter = this.ValidationErrorFormatter
End Property

Public Property Set ValidationErrorFormatter(ByVal RHS As IValidationErrorFormatter)
    Set this.ValidationErrorFormatter = RHS
End Property

Public Property Get Converter() As IValueConverter
    Set Converter = this.Converter
End Property

Public Property Set Converter(ByVal RHS As IValueConverter)
    Set this.Converter = RHS
End Property

Public Property Get Manager() As IBindingManager
    Set Manager = this.Manager
End Property

Public Property Set Manager(ByVal RHS As IBindingManager)
    GuardClauses.GuardDoubleInitialization this.Manager, TypeName(Me)
    GuardClauses.GuardNullReference RHS, TypeName(Me)
    Set this.Manager = RHS
End Property

Private Function TryReadPropertyValue(ByVal Instance As Object, ByVal PropertyName As String, ByRef outValue As Variant) As Boolean
    On Error Resume Next
    outValue = CallByName(Instance, PropertyName, VbGet)
    TryReadPropertyValue = (Err.Number = 0)
    On Error GoTo 0
End Function

Private Function TryWritePropertyValue(ByVal Instance As Object, ByVal PropertyName As String, ByVal Value As Variant) As Boolean
    On Error Resume Next
    CallByName Instance, PropertyName, VbLet, Value
    TryWritePropertyValue = (Err.Number = 0)
    On Error GoTo 0
End Function

Private Sub ApplyToSource()
'reads from the target and writes to the source.

    'one-time and one-way binding modes do not write values to the binding source (ViewModel).
    If this.Mode = OneTimeBinding Or this.Mode = OneWayBinding Then Exit Sub
    
    Dim Target As Object
    Set Target = ResolvePropertyPath(this.Target, this.TargetProperty)
    
    Dim TargetPropertyName As String
    TargetPropertyName = ResolvePropertyName(this.TargetProperty)
    
    Dim TargetValue As Variant
    If Not TryReadPropertyValue(Target, TargetPropertyName, outValue:=TargetValue) Then
        Debug.Print TypeName(Me) & ":[" & ToString & "] failed to read target property value."
        Exit Sub
    End If
    
    If Not this.Converter Is Nothing Then TargetValue = this.Converter.ConvertBack(TargetValue)
    Validate TargetValue
    
    Dim Source As Object
    Set Source = ResolvePropertyPath(this.Source, this.SourcePropertyPath)
    
    Dim SourcePropertyName As String
    SourcePropertyName = ResolvePropertyName(this.SourcePropertyPath)
    
    Dim CurrentValue As Variant
    If Not TryReadPropertyValue(Source, SourcePropertyName, outValue:=CurrentValue) Then
        Debug.Print TypeName(Me) & ":[" & ToString & "] failed to read current source property value."
        Exit Sub
    End If
    
    If TargetValue <> CurrentValue Then
        
        If Not this.ValidationErrorHandler Is Nothing Then this.ValidationErrorHandler.ClearValidationErrors SourcePropertyName
        If Not TryWritePropertyValue(Source, SourcePropertyName, TargetValue) Then
            Debug.Print TypeName(Me) & ":[" & ToString & "] failed to write to source property."
            Exit Sub
        End If
        
        Debug.Print TypeName(Me) & ":[" & ToString & "] was successfully applied to source."
        this.Manager.OnEvaluateCanExecute this.Source
        
    End If
    
End Sub

Private Function ResolvePropertyPath(ByVal Source As Object, ByVal PropertyPath As String) As Object
    
    Dim Parts As Variant
    Parts = Strings.Split(PropertyPath, ".")
    
    If UBound(Parts) = LBound(Parts) Then
        Set ResolvePropertyPath = Source
    Else
        Dim RecursiveProperty As Object
        Set RecursiveProperty = CallByName(Source, Parts(0), VbGet)
        If RecursiveProperty Is Nothing Then Exit Function
        Set ResolvePropertyPath = ResolvePropertyPath(RecursiveProperty, Right$(PropertyPath, Len(PropertyPath) - Len(Parts(0)) - 1))
    End If
    
End Function

Private Function ResolvePropertyName(ByVal PropertyPath As String) As String
    Dim Parts As Variant
    Parts = Strings.Split(PropertyPath, ".")
    ResolvePropertyName = Parts(UBound(Parts))
End Function

Private Sub Apply()
'reads from the source and writes to the target.
    
    'one-way to source and already-applied one-time binding modes do not apply to target
    If this.Mode = OneWayToSource Or (this.Mode = OneTimeBinding And this.Applied) Then Exit Sub
    
    Dim Source As Object
    Set Source = ResolvePropertyPath(this.Source, this.SourcePropertyPath)
    
    Dim SourceValue As Variant
    Dim UseFallbackDefault As Boolean
    
    If Source Is Nothing Then
        UseFallbackDefault = TryGetDefaultBindingValue(outValue:=SourceValue)
    End If
    
    If Source Is Nothing And Not UseFallbackDefault Then
        Debug.Print "Cannot bind target property '" & this.TargetProperty & "' (" & TypeName(this.Target) & ": '" & this.Target.Name & "'); source object in path '" & this.SourcePropertyPath & "' is Nothing"
        Exit Sub
    ElseIf UseFallbackDefault Then
        Debug.Print "Source object in path '" & this.SourcePropertyPath & "' is Nothing; binding target property '" & this.TargetProperty & "' to default/fallback value."
    End If
    
    Dim SourcePropertyName As String
    SourcePropertyName = ResolvePropertyName(this.SourcePropertyPath)
    
    On Error Resume Next
    If Not UseFallbackDefault Then
        SourceValue = CallByName(Source, SourcePropertyName, VbGet)
        If Err.Number <> 0 Then
            Debug.Print "Failed to apply binding for target property '" & this.TargetProperty & "' (" & TypeName(this.Target) & ": '" & this.Target.Name & "'); source path: '" & this.SourcePropertyPath & "'; " & Err.Description
            On Error GoTo 0
            Err.Clear
            Exit Sub
        End If
    End If
    On Error GoTo 0
    
    Validate SourceValue
    
    Dim Target As Object
    Set Target = ResolvePropertyPath(this.Target, this.TargetProperty)
    Debug.Assert Not Target Is Nothing
    
    If Not this.Converter Is Nothing Then
        SourceValue = this.Converter.Convert(SourceValue)
        'TODO: handle converter errors?
    End If
    
    Dim TargetPropertyName As String
    TargetPropertyName = ResolvePropertyName(this.TargetProperty)
    
    Dim CurrentValue As Variant
    CurrentValue = CallByName(Target, TargetPropertyName, VbGet)
    
    If SourceValue <> CurrentValue Then
        On Error Resume Next
        CallByName Target, TargetPropertyName, VbLet, SourceValue
        If Err.Number <> 0 Then
            Debug.Print "Failed to apply binding for target property '" & this.TargetProperty & "' (source path: " & this.SourcePropertyPath & "): " & Err.Description
            Err.Clear
        Else
            Debug.Print "Successfully updated binding for target property '" & this.TargetProperty & "' (source path: " & this.SourcePropertyPath & ")"
            If Not this.ValidationErrorHandler Is Nothing Then this.ValidationErrorHandler.ClearValidationErrors SourcePropertyName
            this.Manager.OnEvaluateCanExecute this.Source
        End If
        On Error GoTo 0
    End If
End Sub

Private Function TryGetDefaultBindingValue(ByRef outValue As Variant) As Boolean
'Gets a default value for certain specific target properties, used when source path cannot be fully resolved,
'e.g. when target binds to "SomeObjectProperty.SomeProperty" and "SomeObjectProperty" is Nothing.

    Select Case ResolvePropertyName(this.TargetProperty)
    
        Case "Text", "Caption"
            outValue = vbNullString
            TryGetDefaultBindingValue = True
            
        Case "Enabled", "Visible"
            outValue = False
            TryGetDefaultBindingValue = True
            
        Case "Value"
            If TypeOf this.Target Is MSForms.CheckBox _
                Or TypeOf this.Target Is MSForms.OptionButton _
            Then
                outValue = False
                TryGetDefaultBindingValue = True
            End If
            
    End Select
End Function

Private Sub Validate(ByVal TargetValue As Variant)
    If this.Validator Is Nothing Then Exit Sub
    
    If this.Validator.IsValid(TargetValue, this.Source, this.Target) Then
        Debug.Print TypeName(Me) & ":[" & ToString & "] value passed validation."
        If Not this.ValidationErrorFormatter Is Nothing Then
            this.ValidationErrorFormatter.Restore
        End If
        Exit Sub
    
    End If
    
    Debug.Print TypeName(Me) & ":[" & ToString & "] value failed validation and will not be applied. A validation error will be propagated."
    If Not this.ValidationErrorFormatter Is Nothing Then
        this.ValidationErrorFormatter.Apply ResolvePropertyName(this.SourcePropertyPath), ValidationErrorMessage
    End If

    NotifyValidationError
    'Err.Raise ValidationError, TypeName(Me), ValidationErrorMessage
End Sub

Private Property Get ValidationErrorMessage() As String
    If Not this.Validator Is Nothing Then
        ValidationErrorMessage = this.Validator.Message
    'Else: no validator -> value is always valid -> no need for a validation error message
    End If
End Property

Private Sub NotifyValidationError()
    If this.ValidationErrorHandler Is Nothing Then Exit Sub
    this.ValidationErrorHandler.OnValidationError Me, ValidationErrorMessage
End Sub

Private Sub NotifyPropertyChanged(ByVal Source As Object, ByVal PropertyName As String)
    Dim Handler As IHandlePropertyChanged
    For Each Handler In this.Handlers
        Handler.OnPropertyChanged Source, PropertyName
    Next
End Sub

Private Sub Class_Initialize()
    If Not Me Is PropertyBinding Then Set this.Handlers = New Collection
End Sub

Private Sub IHandlePropertyChanged_OnPropertyChanged(ByVal Source As Object, ByVal PropertyName As String)
    If PropertyName = ResolvePropertyName(this.SourcePropertyPath) Then Apply
    NotifyPropertyChanged Source, PropertyName
End Sub

Private Sub INotifyPropertyChanged_OnPropertyChanged(ByVal Source As Object, ByVal PropertyName As String)
    NotifyPropertyChanged Source, PropertyName
End Sub

Private Sub INotifyPropertyChanged_RegisterHandler(ByVal Handler As IHandlePropertyChanged)
    this.Handlers.Add Handler
End Sub

Private Sub IPropertyBinding_Apply()
    On Error GoTo ValidationFailed
    Apply
    Exit Sub
ValidationFailed:
    Debug.Print TypeName(Me) & ": validation failed, binding was not applied."
End Sub

Private Property Get IPropertyBinding_Converter() As IValueConverter
    Set IPropertyBinding_Converter = this.Converter
End Property

Private Property Get IPropertyBinding_Source() As Object
    Set IPropertyBinding_Source = this.Source
End Property

Private Property Get IPropertyBinding_SourcePropertyPath() As String
    IPropertyBinding_SourcePropertyPath = this.SourcePropertyPath
End Property

Private Property Get IPropertyBinding_Target() As Object
    Set IPropertyBinding_Target = this.Target
End Property

Private Property Get IPropertyBinding_TargetProperty() As String
    IPropertyBinding_TargetProperty = this.TargetProperty
End Property

Private Property Get IPropertyBinding_Mode() As BindingMode
    IPropertyBinding_Mode = this.Mode
End Property

Private Property Get IPropertyBinding_UpdateSourceTrigger() As BindingUpdateSourceTrigger
    IPropertyBinding_UpdateSourceTrigger = this.UpdateSourceTrigger
End Property

Private Sub CheckBoxEventSource_Change()
    On Error GoTo FailedValidation
    If this.UpdateSourceTrigger = OnPropertyChanged Then ApplyToSource
    Exit Sub
FailedValidation:
    On Error GoTo 0
End Sub

Private Sub ComboBoxEventSource_Change()
    On Error GoTo FailedValidation
    If this.UpdateSourceTrigger = OnPropertyChanged Then ApplyToSource
    Exit Sub
FailedValidation:
    On Error GoTo 0
End Sub

Private Sub ListBoxEventSource_Change()
    On Error GoTo FailedValidation
    If this.UpdateSourceTrigger = OnPropertyChanged Then ApplyToSource
    Exit Sub
FailedValidation:
    On Error GoTo 0
End Sub

Private Sub OptionButtonEventSource_Change()
    On Error GoTo FailedValidation
    If this.UpdateSourceTrigger = OnPropertyChanged Then ApplyToSource
    Exit Sub
FailedValidation:
    On Error GoTo 0
End Sub

Private Sub TextBoxEventSource_Change()
    On Error GoTo FailedValidation
    If this.UpdateSourceTrigger = OnPropertyChanged Then ApplyToSource
    Exit Sub
FailedValidation:
    On Error GoTo 0
End Sub

Private Sub TextBoxEventSource_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
    On Error GoTo FailedValidation
    If this.UpdateSourceTrigger = OnKeyPress Then ApplyToSource
    Exit Sub
FailedValidation:
    KeyCode.Value = 0 'swallow invalid keypress
    On Error GoTo 0
End Sub

Private Sub ControlEventSource_Exit(ByVal Cancel As MSForms.ReturnBoolean)
    On Error GoTo FailedValidation
    If this.UpdateSourceTrigger = OnExit Then ApplyToSource
    Exit Sub
FailedValidation:
    Cancel.Value = True 'keep target control focused
    On Error GoTo 0
End Sub

Private Function ToString() As String
    ToString = "source path '" & this.SourcePropertyPath & "' (" & TypeName(this.Source) & "), target property '" & this.TargetProperty & "' (" & TypeName(this.Target) & ")"
End Function

Command Bindings

Command bindings also have a simple interface:

ICommandBinding

'@Folder MVVM.Infrastructure.Bindings
'@ModuleDescription "An object responsible for binding a command to a UI element."
'@Interface
'@Exposed
Option Explicit

'@Description "Gets the event source object bound to the command."
Public Property Get Target() As Object
End Property

'@Description "Gets the command bound to the event source."
Public Property Get Command() As ICommand
End Property

'@Description "Evaluates whether the command can execute given the binding context."
Public Sub EvaluateCanExecute(ByVal Context As Object)
End Sub

...but the implementation feels much simpler here, with 150 lines - but I'm pretty sure I'm only leveraging the simplest binding scenarios, and there should be a way to make this adhere to the Open-Closed Principle (OCP), such that extending it doesn't systematically involve modifying working code:

CommandBinding

'@Folder MVVM.Infrastructure.Bindings
'@ModuleDescription "An object responsible for binding a command to a UI element."
'@PredeclaredId
'@Exposed
Implements ICommandBinding
Option Explicit

Private Type TCommandBinding
    ViewModel As Object
    Target As Object
    Command As ICommand
End Type

Private WithEvents CommandButtonEvents As MSForms.CommandButton
Private WithEvents CheckBoxEvents As MSForms.CheckBox
Private WithEvents ImageEvents As MSForms.Image
Private WithEvents LabelEvents As MSForms.Label

Private this As TCommandBinding

'@Description "Creates and returns an ICommandBinding implementation binding the specified ICommand to the specified MSForms.CommandButton target."
Public Function ForCommandButton(ByVal Target As MSForms.CommandButton, ByVal Command As ICommand, ByVal ViewModel As Object) As ICommandBinding
    GuardClauses.GuardNonDefaultInstance Me, CommandBinding
    Set ForCommandButton = Create(Target, Command, ViewModel)
End Function

'@Description "Creates and returns an ICommandBinding implementation binding the specified ICommand to the specified MSForms.CheckBox target."
Public Function ForCheckBox(ByVal Target As MSForms.CheckBox, ByVal Command As ICommand, ByVal ViewModel As Object) As ICommandBinding
    GuardClauses.GuardNonDefaultInstance Me, CommandBinding
    Set ForCheckBox = Create(Target, Command, ViewModel)
End Function

'@Description "Creates and returns an ICommandBinding implementation binding the specified ICommand to the specified MSForms.Image target."
Public Function ForImage(ByVal Target As MSForms.Image, ByVal Command As ICommand, ByVal ViewModel As Object) As ICommandBinding
    GuardClauses.GuardNonDefaultInstance Me, CommandBinding
    Set ForImage = Create(Target, Command, ViewModel)
End Function

'@Description "Creates and returns an ICommandBinding implementation binding the specified ICommand to the specified MSForms.Label target."
Public Function ForLabel(ByVal Target As MSForms.Label, ByVal Command As ICommand, ByVal ViewModel As Object) As ICommandBinding
    GuardClauses.GuardNonDefaultInstance Me, CommandBinding
    Set ForLabel = Create(Target, Command, ViewModel)
End Function

'@Description "Creates and returns an ICommandBinding implementation binding the specified ICommand to the specified Target."
Public Function Create(ByVal Target As Object, ByVal Command As ICommand, ByVal ViewModel As Object) As ICommandBinding
    GuardClauses.GuardNonDefaultInstance Me, CommandBinding
    
    Dim result As CommandBinding
    Set result = New CommandBinding
    
    Set result.ViewModel = ViewModel
    Set result.Target = Target
    Set result.Command = Command
    
    Set Create = result
    
End Function

Public Property Get ViewModel() As Object
    Set ViewModel = this.ViewModel
End Property

Public Property Set ViewModel(ByVal RHS As Object)
    Set this.ViewModel = RHS
End Property

Public Property Get Target() As Object
    Set Target = this.Target
End Property

Public Property Set Target(ByVal RHS As Object)

    GuardClauses.GuardDoubleInitialization this.Target, TypeName(Me)
    Set this.Target = RHS
    
    Select Case True
        Case TypeOf RHS Is MSForms.CommandButton
            Set CommandButtonEvents = RHS
        
        Case TypeOf RHS Is MSForms.CheckBox
            Set CheckBoxEvents = RHS
        
        Case TypeOf RHS Is MSForms.Image
            Set ImageEvents = RHS
            
        Case TypeOf RHS Is MSForms.Label
            Set LabelEvents = RHS
    
        Case Else
            GuardClauses.GuardExpression _
                Throw:=True, _
                Source:=TypeName(Me), _
                Message:="Type '" & TypeName(RHS) & "' does not support command bindings at the moment."
    End Select

End Property

Public Property Get Command() As ICommand
    Set Command = this.Command
End Property

Public Property Set Command(ByVal RHS As ICommand)
    Set this.Command = RHS
    If Not RHS Is Nothing And Not this.Target Is Nothing Then
        this.Target.ControlTipText = RHS.Description
    End If
End Property

Private Property Get AsInterface() As ICommandBinding
    Set AsInterface = Me
End Property

Private Sub OnExecute()
    If Not this.Command Is Nothing Then
        this.Command.Execute this.ViewModel
    Else
        Debug.Print "BUG in " & TypeName(Me) & ": Command is 'Nothing', cannot execute."
        Debug.Assert False ' should not happen, break here if it does.
    End If
End Sub

Private Property Get ICommandBinding_Target() As Object
    Set ICommandBinding_Target = this.Target
End Property

Private Property Get ICommandBinding_Command() As ICommand
    Set ICommandBinding_Command = this.Command
End Property

Private Sub ICommandBinding_EvaluateCanExecute(ByVal Context As Object)
    EvaluateCanExecute Context
End Sub

Private Sub EvaluateCanExecute(ByVal Source As Object)
    If this.Target Is Nothing Then Exit Sub
    If this.Command Is Nothing Then
        this.Target.Enabled = False
    Else
        this.Target.Enabled = this.Command.CanExecute(Source)
    End If
End Sub

Private Sub CheckBoxEvents_Click()
    GuardClauses.GuardExpression Not TypeOf this.Target Is MSForms.CheckBox, TypeName(Me)
    OnExecute
End Sub

Private Sub CommandButtonEvents_Click()
    GuardClauses.GuardExpression Not TypeOf this.Target Is MSForms.CommandButton, TypeName(Me)
    OnExecute
End Sub

Private Sub ImageEvents_Click()
    GuardClauses.GuardExpression Not TypeOf this.Target Is MSForms.Image, TypeName(Me)
    OnExecute
End Sub

Private Sub LabelEvents_Click()
    GuardClauses.GuardExpression Not TypeOf this.Target Is MSForms.Label, TypeName(Me)
    OnExecute
End Sub

Binding Manager

The BindingManager helper class is how the View can configure the bindings in the code-behind - I gave up trying to find something better than "manager" for a name:

BindingManager

'@Folder MVVM.Infrastructure.Bindings
'@ModuleDescription "Implements helper methods for the View to consume."
'@PredeclaredId
'@Exposed
Option Explicit
Implements IBindingManager
Implements IHandlePropertyChanged

Private Type TState
    Handlers As Collection
    CommandBindings As Collection
    PropertyBindings As Collection
End Type

Private this As TState

Public Function Create() As IBindingManager
    GuardClauses.GuardNonDefaultInstance Me, BindingManager
    Set Create = New BindingManager
End Function

Private Property Get IsDefaultInstance() As Boolean
    IsDefaultInstance = Me Is BindingManager
End Property

Private Sub ApplyBindings(ByVal Source As Object)
    GuardClauses.GuardExpression IsDefaultInstance, TypeName(Me), "Member call is invalid against stateless default instance."
    Dim Binding As IPropertyBinding
    For Each Binding In this.PropertyBindings
        Binding.Apply
    Next
    OnEvaluateCanExecute Source
End Sub

Private Sub OnEvaluateCanExecute(ByVal Source As Object)
    Dim Binding As ICommandBinding
    For Each Binding In this.CommandBindings
        Binding.EvaluateCanExecute Source
    Next
End Sub

Private Sub Class_Initialize()
    If Not IsDefaultInstance Then
        Set this.Handlers = New Collection
        Set this.CommandBindings = New Collection
        Set this.PropertyBindings = New Collection
    End If
End Sub

Private Sub IBindingManager_BindPropertyPath(ByVal Source As Object, ByVal PropertyPath As String, ByVal Target As Object, _
Optional ByVal TargetProperty As String, _
Optional ByVal Mode As BindingMode, _
Optional ByVal Validator As IValueValidator, _
Optional ByVal Converter As IValueConverter, _
Optional ByVal ErrorFormat As IValidationErrorFormatter)
    
    GuardClauses.GuardExpression IsDefaultInstance, TypeName(Me), "Member call is invalid against stateless default instance."
    
    Dim Binding As IPropertyBinding
    
    Select Case True
    
        Case TypeOf Target Is MSForms.CheckBox And (TargetProperty = "Value" Or TargetProperty = vbNullString)
            Set Binding = PropertyBinding.ForCheckBox(Me, Target, Source, PropertyPath, Converter:=Converter, ErrorFormat:=ErrorFormat)
            
        Case TypeOf Target Is MSForms.ComboBox And (TargetProperty = "Value" Or TargetProperty = vbNullString)
            Set Binding = PropertyBinding.ForComboBoxValue(Me, Target, Source, PropertyPath, Converter:=Converter, ErrorFormat:=ErrorFormat)
            
        Case TypeOf Target Is MSForms.Frame And (TargetProperty = "Caption" Or TargetProperty = vbNullString)
            Set Binding = PropertyBinding.ForFrame(Me, Target, Source, PropertyPath, Converter:=Converter, ErrorFormat:=ErrorFormat)
            
        Case TypeOf Target Is MSForms.Label And (TargetProperty = "Caption" Or TargetProperty = vbNullString)
            Set Binding = PropertyBinding.ForLabel(Me, Target, Source, PropertyPath, Converter:=Converter, ErrorFormat:=ErrorFormat)
            
        Case TypeOf Target Is MSForms.ListBox And TargetProperty = "List"
            Set Binding = PropertyBinding.Create(Me, Target, TargetProperty, Source, PropertyPath, Validator:=Validator, Converter:=Converter)
            
        Case TypeOf Target Is MSForms.ListBox And (TargetProperty = "Value" Or TargetProperty = vbNullString)
            Set Binding = PropertyBinding.ForListBoxValue(Me, Target, Source, PropertyPath, Converter:=Converter)
            
        Case TypeOf Target Is MSForms.OptionButton And (TargetProperty = "Value" Or TargetProperty = vbNullString)
            Set Binding = PropertyBinding.ForOptionButton(Me, Target, Source, PropertyPath, Converter:=Converter, ErrorFormat:=ErrorFormat)
            
        Case TypeOf Target Is MSForms.TextBox And (TargetProperty = "Text" Or TargetProperty = vbNullString)
            Set Binding = PropertyBinding.ForTextBox(Me, Target, Source, PropertyPath, Validator:=Validator, Converter:=Converter, ErrorFormat:=ErrorFormat)
            
        Case Else
            Set Binding = PropertyBinding.Create(Me, Target, TargetProperty, Source, PropertyPath, Mode, OnPropertyChanged, Validator, Converter, ErrorFormat)
            
    End Select
    
    this.PropertyBindings.Add Binding
    this.Handlers.Add Binding
    
    If TargetProperty = vbNullString Then
        Debug.Print TypeName(Source) & ": Binding property path '" & PropertyPath & "' to the default-binding property of " & TypeName(Target) & " object '" & Target.Name & "'."
        
    Else
        Debug.Print TypeName(Source) & ": Binding property path '" & PropertyPath & "' to " & TypeName(Target) & " object '" & Target.Name & "' property '" & TargetProperty & "'."
        
    End If
    
End Sub

Private Sub IBindingManager_BindCommand(ByVal Source As Object, ByVal Target As Object, ByVal Command As ICommand)
    
    GuardClauses.GuardExpression IsDefaultInstance, TypeName(Me), "Member call is invalid against stateless default instance."
    
    Dim Binding As ICommandBinding
    
    Select Case True
    
        Case TypeOf Target Is MSForms.CommandButton
            Set Binding = CommandBinding.ForCommandButton(Target, Command, Source)
            
        Case TypeOf Target Is MSForms.Image
            Set Binding = CommandBinding.ForImage(Target, Command, Source)
            
        Case TypeOf Target Is MSForms.Label
            Set Binding = CommandBinding.ForLabel(Target, Command, Source)
            
        Case TypeOf Target Is MSForms.CheckBox
            Set Binding = CommandBinding.ForCheckBox(Target, Command, Source)
            
        Case Else
            GuardClauses.GuardExpression True, TypeName(Source), "Target type '" & TypeName(Target) & "' does not currently support command bindings."
            
    End Select
    
    this.CommandBindings.Add Binding
    Debug.Print TypeName(Source) & ": Binding command '" & TypeName(Command) & "' to " & TypeName(Target) & " object '" & Target.Name & "'."
    
End Sub

Private Sub IBindingManager_ApplyBindings(ByVal Source As Object)
    GuardClauses.GuardExpression IsDefaultInstance, TypeName(Me), "Member call is invalid against stateless default instance."
    ApplyBindings Source
End Sub

Private Sub IBindingManager_OnEvaluateCanExecute(ByVal Source As Object)
    GuardClauses.GuardExpression IsDefaultInstance, TypeName(Me), "Member call is invalid against stateless default instance."
    OnEvaluateCanExecute Source
End Sub

Private Sub IHandlePropertyChanged_OnPropertyChanged(ByVal Source As Object, ByVal PropertyName As String)
    OnEvaluateCanExecute Source
End Sub

There are other components, with data validation being a nicely handled concern that I'll save for another post. The pillar interfaces are IView, IViewModel, ICommand, but user code ViewModels also need to implement INotifyPropertyChanged:

INotifyPropertyChanged

'@Folder MVVM.Infrastructure.Bindings
'@ModuleDescription "An object that can notify registered handlers when a property value changes."
'@Interface
'@Exposed
Option Explicit

'@Description "Registers the specified handler."
Public Sub RegisterHandler(ByVal Handler As IHandlePropertyChanged)
End Sub

'@Description "Notifies all registered handlers of a property value change."
Public Sub OnPropertyChanged(ByVal Source As Object, ByVal PropertyName As String)
End Sub

User code ViewModels also need to implement the IViewModel interface, which is almost a marker interface, except it's a cornerstone of the data validation functionality:

IViewModel

'@Exposed
'@Folder MVVM.Infrastructure
'@ModuleDescription "Formalizes the basic functionality of a ViewModel."
'@Interface
Option Explicit

'@Description "Gets the validation error handler for this ViewModel."
Public Property Get Validation() As IHandleValidationError
End Property

The API comes with AcceptCommand and CancelCommand convenient implementations of ICommand that can easily be command-bound to a CommandButton control:

CancelCommand

'@Folder MVVM.Infrastructure.Commands
'@ModuleDescription "A command that closes (hides) a cancellable View in a cancelled state."
'@PredeclaredId
'@Exposed
Option Explicit
Implements ICommand

Private Type TState
    View As ICancellable
End Type

Private this As TState

'@Description "Creates a new instance of this command."
Public Function Create(ByVal View As ICancellable) As ICommand
    Dim result As CancelCommand
    Set result = New CancelCommand
    Set result.View = View
    Set Create = result
End Function

Public Property Get View() As ICancellable
    Set View = this.View
End Property

Public Property Set View(ByVal RHS As ICancellable)
    GuardClauses.GuardDoubleInitialization this.View, TypeName(Me)
    Set this.View = RHS
End Property

Private Function ICommand_CanExecute(ByVal Context As Object) As Boolean
    ICommand_CanExecute = True
End Function

Private Property Get ICommand_Description() As String
    ICommand_Description = "Cancel pending changes and close."
End Property

Private Sub ICommand_Execute(ByVal Context As Object)
    this.View.OnCancel
End Sub

User code needs to implement their own commands, and I'm hoping the API is simple enough to work with. The user code ViewModel might look like this:

ExampleViewModel

'@Folder MVVM.Example
'@ModuleDescription "An example ViewModel implementation for some dialog."
'@PredeclaredId
Implements IViewModel
Implements INotifyPropertyChanged
Option Explicit

Public Event PropertyChanged(ByVal Source As Object, ByVal PropertyName As String)

Private Type TViewModel
    
    'INotifyPropertyChanged state:
    Handlers As Collection
    
    'CommandBindings:
    SomeCommand As ICommand
    
    'Read/Write PropertyBindings:
    SourcePath As String
    SomeOption As Boolean
    SomeOtherOption As Boolean
    
End Type

Private this As TViewModel
Private WithEvents ValidationHandler As ValidationManager

Public Function Create() As IViewModel
    GuardClauses.GuardNonDefaultInstance Me, ExampleViewModel, TypeName(Me)
    
    Dim result As ExampleViewModel
    Set result = New ExampleViewModel
    
    Set Create = result
End Function

Public Property Get Validation() As IHandleValidationError
    Set Validation = ValidationHandler
End Property

Public Property Get SourcePath() As String
    SourcePath = this.SourcePath
End Property

Public Property Let SourcePath(ByVal RHS As String)
    If this.SourcePath <> RHS Then
        this.SourcePath = RHS
        OnPropertyChanged "SourcePath"
    End If
End Property

Public Property Get SomeOption() As Boolean
    SomeOption = this.SomeOption
End Property

Public Property Let SomeOption(ByVal RHS As Boolean)
    If this.SomeOption <> RHS Then
        this.SomeOption = RHS
        OnPropertyChanged "SomeOption"
    End If
End Property

Public Property Get SomeOtherOption() As Boolean
    SomeOtherOption = this.SomeOtherOption
End Property

Public Property Let SomeOtherOption(ByVal RHS As Boolean)
    If this.SomeOtherOption <> RHS Then
        this.SomeOtherOption = RHS
        OnPropertyChanged "SomeOtherOption"
    End If
End Property

Public Property Get SomeCommand() As ICommand
    Set SomeCommand = this.SomeCommand
End Property

Public Property Set SomeCommand(ByVal RHS As ICommand)
    Set this.SomeCommand = RHS
End Property

Public Property Get SomeOptionName() As String
    SomeOptionName = "Auto"
End Property

Public Property Get SomeOtherOptionName() As String
    SomeOtherOptionName = "Manual/Browse"
End Property

Public Property Get Instructions() As String
    Instructions = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
End Property

Private Sub OnPropertyChanged(ByVal PropertyName As String)
    RaiseEvent PropertyChanged(Me, PropertyName)
    Dim Handler As IHandlePropertyChanged
    For Each Handler In this.Handlers
        Handler.OnPropertyChanged Me, PropertyName
    Next
End Sub

Private Sub Class_Initialize()
    Set this.Handlers = New Collection
    Set ValidationHandler = ValidationManager.Create
End Sub

Private Sub INotifyPropertyChanged_OnPropertyChanged(ByVal Source As Object, ByVal PropertyName As String)
    OnPropertyChanged PropertyName
End Sub

Private Sub INotifyPropertyChanged_RegisterHandler(ByVal Handler As IHandlePropertyChanged)
    this.Handlers.Add Handler
End Sub

Private Property Get IViewModel_Validation() As IHandleValidationError
    Set IViewModel_Validation = ValidationHandler
End Property

Private Sub ValidationHandler_PropertyChanged(ByVal Source As Object, ByVal PropertyName As String)
    OnPropertyChanged PropertyName
End Sub

Obviously MVVM is a (very) steep departure from Smart UI or even Model-View-Presenter, so I'm looking for the simplest possible constraints on user code, without sacrificing too much flexibility and of course all while making it as easy to extend as possible.

All feedback welcome, from minor nitpicks to refactoring & enhancement ideas in any area!

\$\endgroup\$
  • \$\begingroup\$ This code has been uploaded to GitHub \$\endgroup\$ – Mathieu Guindon Sep 13 at 21:27
  • 1
    \$\begingroup\$ Man...this is good stuff!! I will likely post a review as soon as I get some free time, but 2 things stand out to me. 1.PropertyBinding indeed smells, and I do believe that it should be split it into separate classes. You could utilize composition by creating an interface called IPropertyBindingBase, with a concrete implementation of PropertyBindingBase that holds all common functionality. Then have implementations like I<ControlType>PropertyBinding, each of which would be injected with an instance of the base class. (See next comment for #2) \$\endgroup\$ – rickmanalexander Sep 14 at 12:29
  • 2
    \$\begingroup\$ As for your , <Something>Manager naming dilemma, you could try using <Something>Coordinator as an alternative. It would at least get you away from using the ever ambiguous Manager naming convention...then again, is does if used too frequently or as a filler name, then you would right back in the same situation. Naming is hard. \$\endgroup\$ – rickmanalexander Sep 14 at 12:38
  • \$\begingroup\$ Ah, re-reading the code I just picked up that ApplyToSource is using the extracted TryRead and TryWrite helper functions, but Apply was left behind and still uses inline error-handling - that's a low-hanging fruit right there, Apply can easily get a bit cleaner by leveraging these functions. \$\endgroup\$ – Mathieu Guindon Sep 14 at 13:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.