Define Click event on programmatically defined WPF GridViewColumnHeader - wpf

I am programmatically creating a GridViewColumn in WPF as follows:
Dim oGVCol As GridViewColumn = New GridViewColumn
Dim oHeaderTemplate As DataTemplate
oHeaderTemplate = New DataTemplate
Dim oGridFactory As FrameworkElementFactory = New FrameworkElementFactory(GetType(Grid))
oHeaderTemplate.VisualTree = oGridFactory
oGridFactory.SetValue(Grid.BackgroundProperty, Brushes.Transparent)
oGVCol.HeaderTemplate = oHeaderTemplate
(I have removed irrelevant code to set the content of the grid)
What I can't figure out is how to add a "click" event for the GridViewColumnHeader itself. I can add events to the Grid and any other Controls I added through the Factory objects, no problem. But I'm stuck on how to add an event to the header itself.
If you have a solution in VB.NET, great, but C# is fine too.
One (failed) attempt:
AddHandler TryCast(oGVCol.Header, GridViewColumnHeader).Click, AddressOf HeaderClick
Sadly it turns out that I cannot cast oGVCol.Header to a GridViewColumnHeader.

Ok, it may not be pretty, but I found a pretty decent solution to the problem.
Firstly, when I create the Grid Factory for the root element in the header's Visual Tree, I give it a name
oGridFactory.SetValue(Grid.NameProperty, <column name here>))
(Please note that the Names must have only letters, numbers and underscores so if your data contains column names that don't have those, you'll need to deal with that both here, to convert invalid names to valid ones, and below, to revert them back to their original names if necessary.... I won't detail that functionality here)
Also, I add an event handler to the Root "Grid" in the Template for the column header:
oGridFactory.AddHandler(Grid.SizeChangedEvent,
New SizeChangedEventHandler(AddressOf ColumnHeaderSizeChanged))
The "magic" happens in the procedure ColumnHeaderSizeChanged. This procedure is called both when the grid is Rendered the first time, but also when the user is manually resizing columns.
Signature:
Private Sub ColumnHeaderSizeChanged(sender As Object, e As SizeChangedEventArgs)
I keep a List(Of GridViewColumnHeaders) which is reset to an empty list when I need to replace the Grid with a different one. In the ColumnHeaderSizeChanged event I then do the following:
The first thing we need to do is get to the Root of the controls in the Column Header. For example, your column header may contain a TextBlock to show a column name, and icons to indicate it's been sorted up or down. That sort of thing. When the user clicks on the header they may be clicking on any of those controls, so:
Dim oParent As Object
Dim oColHeader As GridViewColumnHeader = Nothing
Dim sColHeaderName As String = String.Empty
Dim oGWH As Grid = Nothing
oParent = e.OriginalSource 'This may be any of the controls in the header.
If Not oParent Is Nothing Then
Try
While Not oParent.Parent Is Nothing
'So we keep going down the Tree until we hit the Root Parent
'which will be the main Grid created in the Grid Factory
oParent = oParent.Parent
End While
Catch
End Try
End If
'But at this point, if we still have a control, it will be the main Grid
If oParent Is Nothing Then
Exit Sub
End If
If TryCast(oParent, Grid) Is Nothing Then
'what the heck is this? This SHOULD be the Grid at the root of the Visual Tree,
'so if, for whatever reason, this is NOT a Grid, get outta here.
Exit Sub
End If
By this point we're on the main Grid, but now we need to get the GridViewColumnHeader into which this Grid has been created. So now we go to the TemplatedParent
While Not oParent.TemplatedParent Is Nothing
oParent = oParent.TemplatedParent
oColHeader = TryCast(oParent, GridViewColumnHeader)
If Not oColHeader Is Nothing Then
'This procedure is called both when the Grid View is first rendered,
'and when the user is dragging the column width.
If Mouse.LeftButton = MouseButtonState.Pressed Then
'Do something appropriate to when the user is resizing the column
Else
'Do something appropriate to when the grid
'is first Rendered or re-displayed
End If
Exit While
End If
End While
At this point we have the GridViewColumnHeader we need, so we can add it to the List and add a Handler for its Click event. moColHeaders is the List(Of GridViewColumnHeaders)
If Not oColHeader Is Nothing Then
If Not moColHeaders.Contains(oColHeader) Then
oColHeader.Name = <column name here>
moColHeaders.Add(oColHeader) 'Only need to add it once!
AddHandler oColHeader.Click, AddressOf HeaderClick
End If
End If
Now you can code your HeaderClick procedure to handle the GridViewColumnHeader.Click event.
Signature:
Private Sub HeaderClick(sender As Object, e As RoutedEventArgs)

Related

How do I change the style of FindTextBox of FlowDocumentReader to match the theme of my app?

I have a WPF app with a FlowDocumentReader control and when I click the search button to search the text, the text box doesn't match the theme of the app. The text does, but the background of the text box doesn't. So if I am using a dark theme, the text I type into the text box is white (which is correct), but the text box background is also white (incorrect, making text not legible).
Does anyone know how to fix this? I tried applying a style but I don't know which component to target.
Wow, Microsoft really does not make this easy.
My Process
I tried the easy trick of adding a Style TargeType="TextBox" to FlowDocumentReader.Resources, but that doesn't work.
I tried doing things the "right" way and overriding FlowDocumentReader's ControlTemplate, but the TextBox in question isn't even part of the ControlTemaplte! Instead, there's a Border named PART_FindToolBarHost. The TextBox we want is added as a child to PART_FindToolBarHost in code- but only after the user has clicked the "find" button (the one with the magnifying glass icon). You can see this for yourself by looking at the control's source code.
With no more XAML-only ideas, I had to resort to using code. We need to somehow get a reference to the TextBox being created, and I can't think of any better way than to manually search the visual tree for it. This is complicated by the fact that the TextBox only exists once the find command has been executed.
Specifically, the find button in FlowDocumentReader binds to ApplicationCommands.Find. I tried adding a CommandBinding for that command to FlowDocumentReader so I could use it as a trigger to retrieve the TextBox. Unfortunately, adding such a CommandBinding somehow breaks the built-in functionality and prevents the TextBox from being generated at all. This breaks even if you set e.Handled = False.
Luckily, though, FlowDocumentReader exposes an OnFindCommand- except, of course, it's a Protected method. So I finally gave in and decided to inherit FlowDocumentReader. OnFindCommand works as a reliable trigger, but it turns out the TextBox isn't created until after the sub finishes. I was forced to use Dispatcher.BeginInvoke in order to schedule a method to run after the TextBox was actually added.
Using OnFindCommand as a trigger, I was finally able to reliably get a reference to the "find" TextBox, which is actually named FindTextBox.
Now that I can get a reference, we can apply our own Style. Except: FindTextBox already has a Style, so unless we want to override it, we're going to have to merge the two Styles. There's no publicly-accessible method for this (even though WPF does this internally in places), but luckily I already had some code for this.
The Working Code
First, a Module with the helper methods I used:
FindVisualChild is used to loop through the visual tree and get a reference to FindTextBox.
MergeStyles is used to combine the existing Style with the Style we supply, once we have that reference.
Module OtherMethods
<Extension()>
Public Function FindVisualChild(obj As DependencyObject, Name As String) As FrameworkElement
For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(obj) - 1
Dim ChildObj As DependencyObject = VisualTreeHelper.GetChild(obj, i)
If TypeOf ChildObj Is FrameworkElement AndAlso DirectCast(ChildObj, FrameworkElement).Name = Name Then Return ChildObj
ChildObj = FindVisualChild(ChildObj, Name)
If ChildObj IsNot Nothing Then Return ChildObj
Next
Return Nothing
End Function
Public Function MergeStyles(ByVal style1 As Style, ByVal style2 As Style) As Style
Dim R As New Style
If style1 Is Nothing Then Throw New ArgumentNullException("style1")
If style2 Is Nothing Then Throw New ArgumentNullException("style2")
If style2.BasedOn IsNot Nothing Then style1 = MergeStyles(style1, style2.BasedOn)
For Each currentSetter As SetterBase In style1.Setters
R.Setters.Add(currentSetter)
Next
For Each currentTrigger As TriggerBase In style1.Triggers
R.Triggers.Add(currentTrigger)
Next
For Each key As Object In style1.Resources.Keys
R.Resources(key) = style1.Resources(key)
Next
For Each currentSetter As SetterBase In style2.Setters
R.Setters.Add(currentSetter)
Next
For Each currentTrigger As TriggerBase In style2.Triggers
R.Triggers.Add(currentTrigger)
Next
For Each key As Object In style2.Resources.Keys
R.Resources(key) = style2.Resources(key)
Next
Return R
End Function
End Module
Then, there's StyleableFlowDocumentReader, which is what I named my extended control that inherits FlowDocumentReader:
Public Class StyleableFlowDocumentReader
Inherits FlowDocumentReader
Protected Overrides Sub OnFindCommand()
MyBase.OnFindCommand()
Dispatcher.BeginInvoke(Sub() GetFindTextBox(), DispatcherPriority.Render)
End Sub
Private Sub GetFindTextBox()
findTextBox = Me.FindVisualChild("FindTextBox")
ApplyFindTextBoxStyle()
End Sub
Private Sub ApplyFindTextBoxStyle()
If findTextBox IsNot Nothing Then
If findTextBox.Style IsNot Nothing AndAlso FindTextBoxStyle IsNot Nothing Then
findTextBox.Style = MergeStyles(findTextBox.Style, FindTextBoxStyle)
Else
findTextBox.Style = If(FindTextBoxStyle, findTextBox.Style)
End If
End If
End Sub
Private findTextBox As TextBox
Public Property FindTextBoxStyle As Style
Get
Return GetValue(FindTextBoxStyleProperty)
End Get
Set(ByVal value As Style)
SetValue(FindTextBoxStyleProperty, value)
End Set
End Property
Public Shared ReadOnly FindTextBoxStyleProperty As DependencyProperty =
DependencyProperty.Register("FindTextBoxStyle",
GetType(Style), GetType(StyleableFlowDocumentReader),
New PropertyMetadata(Nothing, Sub(d, e) DirectCast(d, StyleableFlowDocumentReader).ApplyFindTextBoxStyle()))
End Class
And then, finally, a usage example:
<local:StyleableFlowDocumentReader x:Name="Reader">
<local:StyleableFlowDocumentReader.FindTextBoxStyle>
<Style TargetType="TextBox">
<Setter Property="Foreground" Value="Blue"/>
</Style>
</local:StyleableFlowDocumentReader.FindTextBoxStyle>
<FlowDocument/>
</local:StyleableFlowDocumentReader>

Why is the SelectedItem property of my DataGrid subclass set to Nothing?

I have subclassed the WPF DataGrid in my VB.NET application because I will need to use this component frequently but also need to have some extra features, in this case adding a new row when the tab key is pressed on the bottom right cell.
I have Overidden the OnKeyDown event of the base class. This is being triggered when the tab keys is pressed while the grid is focused, exactly as I want. However, when the event is triggered I need to be able to determine whether I'm on the bottom-right cell or not. To do this I'd like to get the SelectedItem property of my DataGrid and use that to determine which cell is selected.
I am doing all of this programmatically because I don't want users to have to write any more XAML than they would have to for a regular datagrid. It should function in exactly the same way except if you tab on the bottom right cell a new row is added and the first cell of that row is selected. this should apply no matter how many rows/columns the user has in the datagrid.
The code below shows what I want to do but the SelectedItem is not set.
Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
MyBase.OnKeyDown(e)
If e.Key = Key.Tab Then
Dim colIndex As Integer = Me.Columns.IndexOf(Me.CurrentColumn)
Dim colCount As Integer = Me.Columns.Count - 1
If -1 = colIndex Then
'the next line throws a System.NullReferenceException because SelectedItem is not set
If SelectedItem.Equals(Items(Items.Count - 1)) Then
Focus()
Dim dgrCI = New DataGridCellInfo(Items(Items.Count - 1), Columns(colIndex))
ScrollIntoView(Items(Items.Count - 1))
BeginEdit()
End If
End If
End If
End Sub
The SelectedItem property should be set to the last item in the table but is instead set to Nothing. Why is this?
Edit:
The answer from Ppp is correct. it seems that by the time the OnKeydown event is triggered the DataGrid has already lost focus. I resolved this issue by using the OnPreviewKeydown event instead.
When your tab key is pressed in the last cell you basically tab out of the datagrid which is why I am guessing your SelectedItem is null.

Hide the DataGrid while TextBox Loses its Focus If DataGrid is not Focused

I have a textbox and DataGrid
While textbox loses the focus and if DataGrid is not Focused then I want to Hide the DataGrid.
I use the below code.
Private Sub txt_LostFocus(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles txt.LostFocus
If DataGrid1.IsFocused = False Then
DataGrid1.Visibility = Windows.Visibility.Hidden
End If
End Sub
using this code even if I click on any Item on DataGrid the DataGrid hides.
Is there any problem in my code?
I'm not sure what the problem is... the behaviour you describe is consistent with your code.
The behaviour might be different from what you would expect...
I think when the TextBox loses focus the DataGrid will never have focus because the TextBox didn't finish losing focus. Is that the problem?
If that's the problem, you may add some kind of delay before hiding DataGrid (in a non blocking way of course). You can create a new Thread, do a Sleep(500) on that thread before hiding the control, and see what happens.
You also need to take care because only the UI thread may change visible controls, but you may ask further help if you choose to do that.
I hope it helps.
When the textbox lostfocus even fired .. the gridview not focused yet ..
So, add something like this
Dim lDGVFocused as Boolean
Private Sub Datagrid1_Enter( ... ) ...
lDGVFocused = True
End Sub
Private Sub Datagrid1_LostFocus( ... ) ...
lDGVFocused = False
End Sub
Private Sub txt_LostFocus( ... ) ...
If not lDGVFocused then DataGrid1.Visible = False
End Sub
Private Sub txt_GotFocus( ... ) ...
DataGrid1.Visible = True
End Sub

How to retrieve a particular cell value from a wpf datagrid without selecting the cell or the respective row?

I m working with WPF DataGrid and I want to retrieve a DataGridCell's value by using the column and the row indexes : all what I could do is this but it didn't work :
myDatGrid.Columns[0].GetCellContent((DataGridRow)myDatGrid.Items[0]);
Could you please show me the way to realize that
Your cells value is going to be contingent on what the given column is bound to. The entire row will be the instance of your model.
Assume we have a collection of Person classes which we are binding to within our DataGrid.
Person p = ((ContentPresenter)myDatGrid.Columns[0].GetCellContent(myDatGrid.Items[0])).Content;
The Content property is going to return the underlying model for the row. If you wanted to obtain a given property you can do so by directly accessing the underyling object which should implement INotifyPropertyChanged, no need to fool with the DataGrid as you would in a WinForms application.
I tried what has been proposed above and it did not work.
I struggled for a whole day trying to bind a ComboBox and select the correct value in a WPF Datagrid when it first loads, so I thought I would share my solution here in hopes that somebody else could benefit. Sorry about the VB, but the concept should work in C# also.
First, if you are trying to get the values populated and selected when the grid first loads, you need to put your code in the correct event handler: LayoutUpdated. In other event handlers, the GetCellContent function returns Nothing (null). You could also put this code in an event handler which handles an event occurring later, such as a Button.Click event, if that meets your requirements.
Second, the code:
For i As Int32 = 0 To DataGrid1.Items.Count - 1
Dim row As DataGridRow
row = DataGrid1.ItemContainerGenerator.ContainerFromItem(DataGrid1.Items(i))
If row IsNot Nothing AndAlso row.Item IsNot Nothing Then
Dim cp As ContentPresenter = DataGrid1.Columns(3).GetCellContent(DataGrid1.Items(i))
If cp IsNot Nothing AndAlso cp.ContentTemplate IsNot Nothing Then
Dim dt As DataTemplate = cp.ContentTemplate
If dt IsNot Nothing Then
Dim cb As ComboBox = dt.FindName("cbVendorNames", cp)
If cb IsNot Nothing Then
cb.ItemsSource = Vendors
cb.DisplayMemberPath = "VendorName"
cb.SelectedValuePath = "AS_ID"
cb.SelectedValue = "" ' set your selected value here
End If
End If
End If
End If
Next
What this code does is (a) loop through all of the rows in the datagrid, (b) get the ContentPresenter for the cell selected (in this case, cell 3 in each row), (c) get the DataTemplate for the ContentPresenter, and (d) finally, get the reference to the ComboBox. This allowed me to bind the ComboBox to an external datasource (a List(Of Vendor) and select its value. This code worked for me.
I do not find this solution particularly elegant or efficient, but it does work. If somebody has a better solution, please post it.
OK I think I got it , I missed some casts that I should put , because the column I'm using is a DataGridComboBoxColumn
I should actually do this :
((ComboBox)(myDatGrid.Columns[0].GetCellContent(
(TestData)myDatGrid.Items[0]))).SelectedValue.ToString());
Now it works.

Apply and validate a bound DataGridViewComboBoxCell directly upon selection change

I have a windows forms DataGridView that contains some DataGridViewComboBoxCells that are bound to a source collection using DataSource, DisplayMember and ValueMember properties. Currently the the combobox cell commits the changes (i.e. DataGridView.CellValueChanged is raised) only after I click on another cell and the combobox cell loses focus.
How would I ideally commit the change directly after a new value was selected in the combobox.
This behaviour is written into the implementation of the DataGridViewComboBoxEditingControl. Thankfully, it can be overridden. First, you must create a subclass of the aforementioned editing control, overriding the OnSelectedIndexChanged method:
protected override void OnSelectedIndexChanged(EventArgs e) {
base.OnSelectedIndexChanged(e);
EditingControlValueChanged = true;
EditingControlDataGridView.NotifyCurrentCellDirty(true);
EditingControlDataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
This will ensure that the DataGridView is properly notified of the change in item selection in the combo box when it takes place.
You then need to subclass DataGridViewComboBoxCell and override the EditType property to return the editing control subclass from above (e.g. return typeof(MyEditingControl);). This will ensure that the correct editing control is created when the cell goes into edit mode.
Finally, you can set the CellTemplate property of your DataGridViewComboBoxColumn to an instance of the cell subclass (e.g. myDataGridViewColumn.CellTemplate = new MyCell();). This will ensure that the correct type of cell is used for each row in the grid.
I tried using Bradley's suggestion, but it was sensitive to when you attached the cell template. It seemed like I couldn't allow the design view to wire up the column, I had to do it myself.
Instead, I used the binding source's PositionChanged event, and triggered updates from that. It's a little bit odd, because the control is still in edit mode, and the databound object doesn't get the selected value yet. I just updated the bound object myself.
private void bindingSource_PositionChanged(object sender, EventArgs e)
{
(MyBoundType)bindingSource.Current.MyBoundProperty =
((MyChoiceType)comboBindingSource.Current).MyChoiceProperty;
}
A better way to achieve this that I am using successfully rather than subclassing or the somewhat inelegant binding source method above, is the following (sorry it's VB but if you can't translate from VB to C# you have bigger problems :)
Private _currentCombo As ComboBox
Private Sub grdMain_EditingControlShowing(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles grdMain.EditingControlShowing
If TypeOf e.Control Is ComboBox Then
_currentCombo = CType(e.Control, ComboBox)
AddHandler _currentCombo.SelectedIndexChanged, AddressOf SelectionChangedHandler
End If
End Sub
Private Sub grdMain_CellEndEdit(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles grdMain.CellEndEdit
If Not _currentCombo Is Nothing Then
RemoveHandler _currentCombo.SelectedIndexChanged, AddressOf SelectionChangedHandler
_currentCombo = Nothing
End If
End Sub
Private Sub SelectionChangedHandler(ByVal sender As Object, ByVal e As System.EventArgs)
Dim myCombo As ComboBox = CType(sender, ComboBox)
Dim newInd As Integer = myCombo.SelectedIndex
//do whatever you want with the new value
grdMain.NotifyCurrentCellDirty(True)
grdMain.CommitEdit(DataGridViewDataErrorContexts.Commit)
End Sub
That's it.

Resources