Tuesday, October 4, 2022

Boxing and unboxing properties of objects

This blog entry uses Visual Basic because the problem it solves doesn't exist in C#.

This time last year I wrote a blog entry about the perils of passing data row fields to SetProperty in the MVVM model. A colleague recently asked me about a problem he was having when passing an object's property to SetProperty - the symptoms were very similar. 

In his situation he had a class called ClientServer that contained properties and was passed between the client and the server. It did not implement INotifyPropertyShared. Then he had a wrapper class called Client that had an instance of ClientServer as a private member and used it as backing store for properties in Client that did implement INotifyPropertyChanged.

The bound control was always showing the prior value of the property instead of the current value. His Prop property in Client looked like this...

Public Property Prop As String
    Get
        Return ClientServer.Prop
    End Get
    Set(value As String)
        SetProperty(ClientServer.Prop, value)
    End Set
End Property

Public Function SetProperty(Of T)(ByRef storage As T, value As T, <CallerMemberName> Optional name As String = "") As Boolean
    If (Object.Equals(storage, value)) Then Return False
    storage = value
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    Return True
End Function

It occurred to me that we could enhance SetProperty to detect when it was called with an object and use reflection to determine if name is a property of that object. If so, we would use reflection to update the property. This would bypass the boxing of storage and solve our problem. The only constraint is that the property of Client must have the same name as the property in ClientServer that it shadows. Otherwise the ClientServer property name would have to be passed as the third parameter of SetProperty.

Public Property Prop As String
    Get
        Return ClientServer.Prop
    End Get
    Set(value As String)
        SetProperty(ClientServer, value)
    End Set
End Property

Public Function SetProperty(Of T)(ByRef storage As T, value As Object, <CallerMemberName> Optional name As String = "") As Boolean
    Dim type As Type = storage.GetType()
    Dim PI As PropertyInfo = type.GetProperty(name)
 
    If type.IsClass AndAlso PI IsNot Nothing Then
        If Object.Equals(PI.GetValue(storage), value) Then Return False
        PI.SetValue(storage, value, Nothing)
    Else
        If (Object.Equals(storage, value)) Then Return False
        storage = Convert.ChangeType(value, type)
    End If
    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
    Return True
End Function

You can see this working by starting a new Visual Studio 2022 WPF Framework project in Visual Basic. Call it ObjectBoxingVB.

Replace the contents of MainWindow.xaml with this...

<Window x:Class="ObjectBoxingVB.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:ObjectBoxingVB"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Object Boxing" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>
 
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Enter Text"/>
        <TextBlock Grid.Row="1" Grid.Column="0" Text="Prop Bound Right"/>
        <TextBlock Grid.Row="2" Grid.Column="0" Text="Prop Bound Wrong"/>
 
        <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SomeText, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
        <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Client.Prop}" Width="100" Background="Tan"/>
        <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Client.PropDoneWrong}" Width="100" Background="Tan"/>
    </Grid>
</Window>
 
Replace the contents of MainWindow.xaml.vb with this.

Imports System.ComponentModel
Imports System.Configuration
Imports System.Reflection
Imports System.Runtime.CompilerServices
Imports System.Windows
 
Namespace ObjectBoxingVB
    Public Class cClientServer
        Public Property Prop As String = ""
        Public Property PropDoneWrong As String = ""
    End Class
 
    Public Class cClient
        Implements INotifyPropertyChanged
 
        Private ClientServer As cClientServer = New cClientServer()
        Public Property Prop As String
            Get
                Return ClientServer.Prop
            End Get
            Set(value As String)
                SetProperty(ClientServer, value)
            End Set
        End Property
 
        Public Property PropDoneWrong As String
            Get
                Return ClientServer.PropDoneWrong
            End Get
            Set(value As String)
                SetProperty(ClientServer.PropDoneWrong, value)
            End Set
        End Property
 
        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
        Public Function SetProperty(Of T)(ByRef storage As T, value As Object, <CallerMemberName> Optional name As String = "") As Boolean
            Dim type As Type = storage.GetType()
            Dim PI As PropertyInfo = type.GetProperty(name)
 
            If type.IsClass AndAlso PI IsNot Nothing Then
                If Object.Equals(PI.GetValue(storage), value) Then Return False
                PI.SetValue(storage, value, Nothing)
            Else
                If (Object.Equals(storage, value)) Then Return False
                storage = Convert.ChangeType(value, type)
            End If
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))
            Return True
        End Function
    End Class
 
    Partial Public Class MainWindow
        Inherits Window
 
        Public Property Client As New cClient()
 
        Private _SomeText As String = ""
        Public Property SomeText As String
            Get
                Return _SomeText
            End Get
            Set(value As String)
                _SomeText = value
                Client.Prop = SomeText
                Client.PropDoneWrong = SomeText
            End Set
        End Property
 
        Public Sub New()
            InitializeComponent()
        End Sub
 
    End Class
End Namespace

Run the application and start typing in the text box. You will see the lower textblock lags one character behind the input.



No comments:

Post a Comment