Scenario
I am implementing Search and Replace functionality in my DataGrid (WPFToolkit June 2009 release). I started off by using the Search and Highlight technique by Shamman Tomer on his blog WPF Essentials which works a treat. I then wanted a CheckBoxColumn called ‘Replace’ in my DataGrid with the CheckBox to automatically become visible and checked when rows in the Highlighted Column became highlighted. The rest of this article will assume that you have implement Search and Highlight as per WPF Essentials
Anyway, Easy, I thought, I’ll just bind the CheckBoxColumn to the Attached Property that flags the Highlight Column to become highlighted.
Not so easy. Taxi Driver, start the meter on the Journey Of Pain if you will.
You see, what I didn’t know at this stage was that DataGridColumns are different. Jaime Rodruigez, a WPf Technical Evangelist at Microsoft explains
The Columns collection is just a property in the Datagrid; this collection is not in the logical (or visual) tree, therefore the DataContext is not being inherited, which leads to there being nothing to bind to.
Binding To Attached Property
The syntax for Binding to Attached Properties is straightforward. My Attached Property is ‘IsMatch’ in the FindAndReplace class in my Infrastructure project, so I just went:
wpftoolkit:DataGridCheckBoxColumn Header="Replace"
CellStyle="{DynamicResource IsReplaceCell}"
Binding="{Binding Path=(inf:FindAndReplace.IsMatch),
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}" /
IsReplaceCell is a copy of Shamam’s Style Resource that sets the IsMatch Property to perform highlighting. I figured that I could bind to this and get a happy little checkmark appearing in my column.
Nope. Didn’t work. As Jaime Rodruigez could’ve told me.
There followed six hours of intense Googlefying and code tinkering that finally alerted me to the existence of DependencyProperty PropertyChangedCallbacks. Yes, this is my first WPF project.
So I added a PropertyChangedCallback to the IsMatch AttachedProperty, which is a DependencyProperty, like so:
'Using a DependencyProperty as the backing store for IsMatch. This enables animation, styling, binding, etc...
Public Shared ReadOnly IsMatchProperty As DependencyProperty = _
DependencyProperty.RegisterAttached("IsMatch", _
GetType(Boolean), _
GetType(SearchOperations), _
New UIPropertyMetadata( _
False, _
New PropertyChangedCallback(AddressOf OnIsMatchChanged)))
A PropertyChangedCallback To Call My Own
The definition of the PropertyChangedCallback is:
'''
''' Callback for IsMatch PropertyChanged
'''
''' </remarks
Private Shared Sub OnIsMatchChanged(ByVal sender As DependencyObject, _
ByVal e As DependencyPropertyChangedEventArgs)
// Do something interesting
End Sub
Takes Two and Three
As you can see there’s heaps of information available to you in the Callback. You get the whole Dependency Property which contains the object causing the Callback, in my case a DataGridCell and also the DependencyPropertyChangedEventArgs which contain the old and new values of the DependencyProperty.
This was all new to me not having performed an autopsy on a DependencyProperty before, but eventually I found out that my DataGridCell object was in there and a couple of interesting Properties such as DataContext and Content.
Based on that I first tried to set the Checkbox to IsChecked by binding it to a Boolean Property in the DataGridCell’s DataContext and implementing IsPropertyChanged. Didn’t work. So, then inspecting the Content Property I found to my joy that it was a CheckBox control, so I was able to set it to the New value of my IsMatch Dependency Property in the PropertyChangedCallback like so:
Private Shared Sub OnIsMatchChanged(ByVal sender As DependencyObject, _
ByVal e As DependencyPropertyChangedEventArgs)
Dim cell As DataGridCell = CType(sender, DataGridCell)
cell.Content.IsChecked = e.NewValue
End Sub
To differentiate the CheckBox column it was necessary to distinguish it from the TextBox (Highlighting) column in the same Grid which also listened to IsMatch.
If TypeOf (cell.Column) Is DataGridCheckBoxColumn Then
cell.Content.IsChecked = e.NewValue
End If
To make sure that only the particular CheckBox columns in my Search and Replace Grids would be affected, I subclassed DataGridCheckBoxColumn to ReplaceDataGridCheckBoxColumn and did the following
If TypeOf (cell.Column) Is ReplaceDataGridCheckBoxColumn Then
cell.Content.IsChecked = e.NewValue
End If
This is not essential as only Checkbox columns that had the IsReplaceStyle would trigger the IsMatch DependencyProperty PropertyChangedCallback, but it made it that little bit more tidy.
This was a major win. I was feeling omnipotent.
Pass The Viagara
Did I say omnipotent? Perhaps I really meant impotent.
The above code works great if your Replace column is always visible. If, however, you want to toggle the Replace column from Visibility.Hidden to Visibility.Visible then the above code crashes during the PropertyChangedCallback.
This is because the Content Property of an invisible DataGridCell is null, so cell.Content.IsChecked throws a Null Reference Exception. After another few hours Googling, waving dead cats at the terminal and smashing the keyboard with a Nerf Claw Hammer I came up with a hack of majestic proportions.
It dawned on me that its only when the CheckBox column is invisible that the Content is Null, so if I run a Dummy search returning zero matches, this will have the effect of making the column Visible and thus Content Is Not Null, then I can run the real Search and make the now visible columsn checked if they match! BWA-HA-HA-HAAAAA-AAAAAAAAAAAAAAAAAAAA...aaaaaaaa......!! It was so horrendous I loved it on sight. Here it is, peformed by Code-Behind for the sheer pleasure of violating the MVVM Design Pattern.
' Copy current Find And Replace so we can put it back after Impossible Search executes
Private Sub tbFindText_LostFocus(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles tbFindText.LostFocus
Dim origFindAndReplace As FindAndReplace = New FindAndReplace
origFindAndReplace.FindText = String.Copy(Me._viewModel.FindAndReplaceViewModel.FindAndReplace.FindText)
origFindAndReplace.ReplaceText = String.Copy(Me._viewModel.FindAndReplaceViewModel.FindAndReplace.ReplaceText)
Me._viewModel.FindAndReplaceViewModel.FindAndReplace.FindText = String.Empty
' Perform Impossible Search using Highest Priority
Application.Current.Dispatcher.Invoke( _
System.Windows.Threading.DispatcherPriority.Send, _
New System.Threading.ThreadStart(AddressOf ExecuteFindTextHandler))
' Perform real search using lowest priority, hoping that Highest Priority Thread terminates first.
' Should really use a Thread callback
Me._viewModel.FindAndReplaceViewModel.FindAndReplace = origFindAndReplace
Me._viewModel.FindAndReplaceViewModel.Visibility = Windows.Visibility.Visible
Application.Current.Dispatcher.Invoke( _
System.Windows.Threading.DispatcherPriority.Background, _
New System.Threading.ThreadStart(AddressOf ExecuteFindTextHandler))
End Sub
CONFESSION: I'm not that cavalier. I did put it in ViewModel afterwards.
The Final DataGrid
Here's the markup for the final dataGrid. If you've copied Shamam Tomer's exampel from WPF essentials this should basically make sense to you. The only bit left to explain is why I am Binding DataGrid Visibility to an x:Static see below:
wpftoolkit:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Path=ModuleContents}"
inf:SearchOperations.SearchTerm="{Binding Path=FindAndReplace.FindText,
UpdateSourceTrigger=PropertyChanged}"
wpftoolkit:DataGrid.Columns
wpftoolkit:DataGridTextColumn Header="Content"
CellStyle="{DynamicResource ContentsCell}"
Binding="{Binding Contents}"
inf:ReplaceDataGridCheckBoxColumn Header="Replace"
Visibility="{Binding Source={x:Static Member=inf:DataGridViewModel.ColumnVisibility}, Path=ReplaceVisibility}"
CellStyle="{DynamicResource IsReplaceCell}"
Binding="{Binding Path=IsReplace, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" /
/wpftoolkit:DataGrid.Columns
/wpftoolkit:DataGrid
Binding DataGrid Visibility To x:Static
Like Jaime Rodruigez says, because dataGridColumns are not in the Visual or Logical tree you can't bind them to Class Properties. If you use Jaime's trick of fowarding the DataGrid's DataContext to the DataGridCell you can then bind the DataGridCell to an Property declared on another Control, but that seemed rather indirect to me. The other options open to you are:
- Binding to a StaticResource
- Binding to a Static Class
I decided binding to a Static Class fitted the MVVM pattern better as Binding to a StaticResource means you can't update that Binding unless you expose the View's StaticResources to its ViewModel, which exposed more detail of the View to the ViewModel than seems advised under MVVM.
In Summary
To wire up a DataGrid for Search and Replace and include optional amazing toggling Replace Column, the following is required:
1. Replace column in DataGrid must be of type ReplaceDataGridCheckedColumn.
2. Static Class to hold Visibility of Replace Column for DataGrid
3. A DataGridCell Style with a Style Trigger bound to the Property containing the Property to be searched. This is often a ‘Contents’ column containing HTML or Text to be Searched
4. A DataGridCell Style for the Replace column which causes automatic checking with a Depeendency Property PropertyChangedCallback
5. Bind DataGrid to the Dependency Properties in the SearchOperations Attached class
6. IsReplace Property implemented in Business Class that the ItemsSource of the DataGrid Binds to.
