One of our engineers had an interesting idea - why not use a data row as the backing store for bound properties? That would eliminate the need for writing code to explode the data row into properties when the data is loaded and vice versa when it is saved. Let's see how that would look. Start Visual Studio and create a Visual Basic WPF Application called Unboxing. The reason for the name will become apparent later.
Change MainWindow.xaml to look like this.
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:Unboxing"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="450" Width="800">
<StackPanel Orientation="Vertical" Width="100" Height="50" Background="PaleGoldenrod" VerticalAlignment="Center">
<TextBox Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"/>
<Separator/>
<TextBlock Text="{Binding Text}"/>
</StackPanel>
</Window>
Implements INotifyPropertyChanged
Private drDocument As DataRow
drDocument("Text") = ""
dtDocument.Rows.Add(drDocument)
InitializeComponent()
End Sub
Get
Return drDocument("Text").ToString()
Set(value As String)
End Property
If Object.Equals(storage, value) Then Return False
storage = value
NotifyPropertyChanged(PropertyName)
Return True
End Function
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(PropertyName))
If you run this and start typing you will notice something is wrong. The text block is always one keystroke behind the text box. Why is this?
Document("Text") is not a string, it's an object of type string. They are not the same thing. When you use Document("Text") as a parameter the compiler adds code to unbox it into a temporary string which gets passed to SetProperty. When SetProperty assigns a value to storage it is only updating the temporary string. Then it raises PropertyChanged which calls the property Getter, which returns drDocument("Text") WHICH HAS NOT BEEN UPDATED, YET.
Then SetProperty exits and the temporary string gets boxed into drDocument("Text"). Because of the implicit unboxing and boxing, the Getter returns the "wrong" value. You can fix this problem by delaying the event, although this may cause other problems. For example, you could replace the RaiseEvent with...
Dispatcher.BeginInvoke(Sub()
This causes the event to be raised when control returns to the framework. In simple cases, this works well, but may break more complex code. Try it now.
Note this is not a problem in C#. When coding this in C# you have to prefix the first parameter to SetProperty as ref. When you do this you get a build error saying "Property or indexer may not be passed as an out or ref parameter".
SetProperty(ref drDocument["Text"], value)
If Not dr.Table.Columns.Contains(ColumnName) Then Throw New ArgumentException(String.Format("Column {0} not found.", ColumnName))
dr(ColumnName) = value
NotifyPropertyChanged(PropertyName)
Return True
End Function
Public Function SetProperty(Of T)(dr As Data.DataRowView, value As T, <System.Runtime.CompilerServices.CallerMemberName> Optional ColumnName As String = Nothing, <System.Runtime.CompilerServices.CallerMemberName> Optional PropertyName As String = Nothing) As Boolean
If Not dr.Row.Table.Columns.Contains(ColumnName) Then Throw New ArgumentException(String.Format("Column {0} not found.", ColumnName))
dr(ColumnName) = value
NotifyPropertyChanged(PropertyName)
Return True
End Function
Get
Return drDocument("Text").ToString()
Set(value As String)
End Set
End Property
When you run it now it works correctly and will also work in more complicated code.