Thursday, June 3, 2021

Two-way Dependency Property

I see a lot of questions in StackOverflow about writing two-way dependency properties. That's a dependency property that can update the bound property as well as reading it. Here's an example of a TextBox that implements an IsDirty dependency property. When the text box is dirty it sets the IsDirty dependency property true and turns the background light yellow. The hosting window can read or write the TextBox's IsDirty property.

Start a new Visual Studio WPF (Framework) application using Visual Basic and call it TwoWayDependencyProperty. The first thing we need to do is write our new TextBox control. Add a class and call it IsDirtyTextBox. It will inherit TextBox.

Imports System.Globalization
 
Public Class IsDirtyTextBox
    Inherits TextBox
 
End Class
 
It will support a new boolean dependency property called IsDirty. We want the hosting window to be able to change its value at run time so we will provide a Changed event handler. Note the use of BindsTwoWayByDefault. IsDirty_Changed will be called whenever the dependency property value is changed whether we did it or the hosting window did.

Public Shared ReadOnly IsDirtyProperty As DependencyProperty = DependencyProperty.Register(NameOf(IsDirty), GetType(Boolean), GetType(IsDirtyTextBox), New FrameworkPropertyMetadata(False, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, AddressOf IsDirty_Changed))
 
Private Shared Function IsDirty_Changed(d As DependencyObject, baseValue As Object) As Object
    Dim istb As IsDirtyTextBox = DirectCast(d, IsDirtyTextBox)
    istb.IsDirty = Convert.ToBoolean(DirectCast(baseValue, DependencyPropertyChangedEventArgs).NewValue)
End Function
 
We need to provide an IsDirty property that is synchronized with the dependency property.

Private _IsDirty As Boolean
Public Property IsDirty As Boolean
    Get
        Return Convert.ToBoolean(GetValue(IsDirtyProperty))
    End Get
    Set(value As Boolean)
        SetCurrentValue(IsDirtyProperty, value)
    End Set
End Property

We will set isDirty true when the user modifies the text. We add an event handler to the TextChanged event to do this. The wiring gets done in the constructor.


Public Sub New()
 
    Dim bBackground As New Binding("IsDirty")
    bBackground.Source = Me
    bBackground.Converter = New IsDirtyConverter()
    bBackground.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
    Me.SetBinding(TextBox.BackgroundProperty, bBackground)
 
    AddHandler Me.TextChanged, AddressOf Text_Changed
End Sub
 
Private Sub Text_Changed(sender As Object, e As TextChangedEventArgs)
    IsDirty = True
End Sub

You will notice that the binding above uses a converter to convert from a boolean to a brush. Here's the converter (put it after theIsDirtyTextBox class).

Public Class IsDirtyConverter
    Implements IValueConverter
 
    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert
        Dim IsDirty As Boolean = System.Convert.ToBoolean(value)
        If IsDirty Then
            Return New SolidColorBrush(Colors.LightGoldenrodYellow)
        Else
            Return New SolidColorBrush(Colors.Transparent)
        End If
    End Function
 
    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class

Now modify MainWindow so the XAML and VB looks like this.

<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:TwoWayDependencyProperty"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Horizontal" Height="20">
        <local:IsDirtyTextBox Width="100" Height="20" IsDirty="{Binding IsDirty, UpdateSourceTrigger=PropertyChanged}"/>
        <TextBlock Text="Is Dirty" Margin="10,0,0,0"/>
        <CheckBox IsChecked="{Binding IsDirty, Mode=TwoWay}"/>
    </StackPanel>
</Window>
 
and this

Imports System.ComponentModel
 
Class MainWindow
    Implements INotifyPropertyChanged
 
    Private _IsDirty As Boolean
    Public Property IsDirty As Boolean
        Get
            Return _IsDirty
        End Get
        Set(value As Boolean)
            SetProperty(_IsDirty, value)
        End Set
    End Property
 
    Public Event PropertyChanged As System.ComponentModel.PropertyChangedEventHandler Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
 
    Public Function SetProperty(Of T)(ByRef storage As T, value As T, <System.Runtime.CompilerServices.CallerMemberName> Optional PropertyName As String = Nothing) As Boolean
        If Object.Equals(storage, value) Then Return False
        storage = value
        NotifyPropertyChanged(PropertyName)
        Return True
    End Function
 
    Public Sub NotifyPropertyChanged(<System.Runtime.CompilerServices.CallerMemberName> Optional PropertyName As String = Nothing)
        If PropertyName.StartsWith("set_", System.StringComparison.OrdinalIgnoreCase) Then
            PropertyName = PropertyName.Substring(4)
        End If
        RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(PropertyName))
    End Sub
 
End Class
 


Here's the initial screen

After changing text. Note the background and IsDirty property have changed

After clearing the checkbox, background color has changed too

You can see that the binding between the IsDirtyTextBox's IsDirty dependency property and the IsDirty property in the hosting window goes both ways. The TextBox can tell the window that it is dirty and the window can tell the TextBox not the be dirty.

No comments:

Post a Comment