One of the things that bugs me still about WPF is the extra effort required to bind properties of DataGridColumns. I noticed that Infragistics has written a custom markup extension to simplify this on their XamDataGrid fields and field settings ie
AllowEdit="{igDP:FieldBinding IsVendorEditable}"
The markup extension only has a constructor and a ProvideValue method. We use the constructor to save off the parameters such as Path, and I planned on finding the DataContext and creating the binding in the ProvideValue method. But ProvideValue is called while the window is still being constructed. I get access to the DataGridColumn but it is also incomplete. In particular, the private DataGridOwner property is null.
So we have to wait until the DataGridColumn is initialized but, hold on, there's no Initialized event! The DataGrid has one, so does the Window but they are what we're trying to find. We have a dilemma here. The DataGridColumn doesn't have what we need and can't tell us when it will have it. We will have to approach this from another direction.
The Application object has a list of windows, one (I assume) of which is currently being initialized. If we add an Initialized event handler to all the windows currently being initialized we can look for our DataGridColumn in all the window's DataGrids and, if we find it, create the binding using that DataGrid's data context.
Time to code. Start a new WPF Framework project in VB and call it ColumnBinding. Add a class called ColumnBinding. Our binding will assume Mode=TwoWay and UpdateSourceTrigger=PropertyChanged. Its only parameter will be the path name. Here's the class and constructor. This code will show an error in the editor.
Imports System.Windows.Markup
Public Class ColumnBindingExtension
Inherits Markup.MarkupExtension
Public Path As String
Public dp As DependencyProperty
Public dc As DataGridColumn = Nothing
Public Sub New(Path As String)
End Class
Dim pvt As IProvideValueTarget = DirectCast(serviceProvider.GetService(GetType(IProvideValueTarget)), IProvideValueTarget)
dc = TryCast(pvt.TargetObject, DataGridColumn)
If dc Is Nothing Then
Throw New Exception("FieldBinding can only be used on DataGridColumn")
End If
dp = DirectCast(pvt.TargetProperty, DependencyProperty)
SetDataContext()
Return DependencyProperty.UnsetValue
End Function
Dim DataContext As Object = Nothing
For Each window As Window In Application.Current.Windows.OfType(Of Window).Where(Function(w) Not w.IsInitialized)
AddHandler window.Initialized, AddressOf CompleteBinding
Next
End Sub
The CompleteBinding method creates the required binding in code. If the window that just initialized contains our target DataColumn then the DataColumn's private DataGridOwner will be populated and we can use the DataGrid's DataContext as our binding's Source.
Private Sub CompleteBinding(sender As Object, e As EventArgs)
Dim dg As DataGrid = TryCast(dc.GetType().GetProperty("DataGridOwner", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(dc), DataGrid)
If dg IsNot Nothing Then
' Create
binding
Dim b As New Binding(Path)
b.Source = dg.DataContext
b.Mode = BindingMode.TwoWay
b.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
BindingOperations.SetBinding(dc, dp, b)
End If
RemoveHandler DirectCast(sender, Window).Initialized, AddressOf CompleteBinding
End Sub
Change MainWindow.xaml to look like this. Our markup extension is in bold.
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:FieldBinding"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel Orientation="Vertical">
<ComboBox ItemsSource="{Binding FontSizes}" SelectedItem="{Binding MyFontSize}" Width="100"/>
<DataGrid.Columns>
<DataGridTextColumn Header="Pet" Binding="{Binding Description}" FontSize="{local:ColumnBinding MyFontSize}"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Window>
Class MainWindow
Implements INotifyPropertyChanged
Public Class cItem
Public Property Description As String
End Class
Private _Items As New List(Of cItem) From
{
New cItem() With {.Description = "Dog"},
Public ReadOnly Property Items As List(Of cItem)
Return _Items
End Property
Private _FontSizes As New List(Of Double) From {10, 12, 14, 16, 20}
Return _FontSizes
End Property
Private _MyFontSize As Double = 10
Get
Return _MyFontSize
Set(value As Double)
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)
PropertyName = PropertyName.Substring(4)
End If
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(PropertyName))
End Class
After selecting 16, the font size changes |
No comments:
Post a Comment