Expand/Collapse TreeView Nodes on Ctrl+LeftMouse in WPF - wpf

I want to collapse or expand all TreeView nodes under parent node if user holds down Left Control key and presses left mouse button on expansion arrow of tree view.
How do you do this in WPF? It's not as obvious as it was in WinForms.

Here's something that seems to work for me:
<TreeView TreeViewItem.Collapsed="TreeViewItem_Collapsed" TreeViewItem.Expanded="TreeViewItem_Expanded"/>
(The above is omitting things like ItemsSource and ItemTemplate for brevity)
Private Sub TreeViewItem_Expanded(sender As Object, e As RoutedEventArgs)
If Keyboard.IsKeyDown(Key.LeftCtrl) Then DirectCast(e.OriginalSource, TreeViewItem).ExpandSubtree()
End Sub
Private Sub TreeViewItem_Collapsed(sender As Object, e As RoutedEventArgs)
If Keyboard.IsKeyDown(Key.LeftCtrl) Then CollapseSubtree(e.OriginalSource)
End Sub
Private Sub CollapseSubtree(Item As TreeViewItem)
Item.IsExpanded = False
For Each Child In Item.ItemContainerGenerator.Items
CollapseSubtree(Item.ItemContainerGenerator.ContainerFromItem(Child))
Next
End Sub
Explanation
Since TreeViewItem.Expanded and TreeViewItem.Collapsed are both routed events, we can handle them at the TreeView level as they "bubble" up. All items in the TreeView will trigger either TreeViewItem_Collapsed or TreeViewItem_Expanded when they close or open, respectively.
In my quick tests, e.OriginalSource always referred to the TreeViewItem that was expanded/collapsed, but some more rigorous testing might be in order.
In the event handlers, we use Keyboard.IsKeyDown to determine whether the left control key is currently being pressed. If so, we take the appropriate action.
For expansion, TreeViewItem already has a convenient ExpandSubtree method, so we can just use that.
For whatever reason, there is no matching CollapseSubtree built in, so I made my own. At its heart, it's a simple recursive method that closes the node you give it and then runs itself again for all child nodes. The trick is in getting the child TreeViewItems (containers), instead of the data objects they are representing. To do this, I make use of the ItemContainerGenerator, which is the class that each TreeViewItem uses to create its children.

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>

Define Click event on programmatically defined WPF GridViewColumnHeader

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)

Avoid expansion of certain TreeNode nodes upon TreeNode.ExpandAll?

Nobody asked that before:
What is an efficient way to avoid the expansion of certain TreeNode class descendants in a WinForms TreeView when the user does the "Expand all" thing, but still let him expand such nodes by clicking on the + symbol?
Sure I can handle BeforeExpand, but I have a hard time setting e.Cancel to true only if it is an ExpandAll operation. I wonder how I can determine this? I could subclass TreeView and override ExpandAll -- but that one cannot be overriden...
Seems like standard .NET treeview doesn`t have the way other than you described: trigger flag before ExpandAll, handle BeforeExpand and enable e.Cancel for appropriate nodes when flag is enabled.
As the ExpandAll method isn`t virtual you have these ways to follow:
Inherit from the TreeView class and add ExpandAllEx method where trigger this flag. No a good one because you need to cast to your tree class everywhere you use the tree instance.
Add an extension method for the TreeView class where use tree.Tag property for this flag. More useful way with minimal changes in existing code.
This works 100%. I think. Sigh.
Private Sub MyTreeViewExpandNodes(ByVal Nodes As TreeNodeCollection)
For Each Node As TreeNode In Nodes
If Not (TypeOf Node Is SpecialTreeNode) Then
Node.Expand()
MyTreeViewExpandNodes(Node.Nodes)
End If
Next
End Sub
Private Sub MyTreeView_KeyDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyTreeView.KeyDown
If e.KeyCode = Keys.Multiply Then
e.Handled = True
e.SuppressKeyPress = True
MyTreeViewExpandNodes(MyTreeView.Nodes)
End If
End Sub

Binding with UpdateSourceTrigger==LostFocus do not fire for Menu or Toolbar interaction

I noticed that bindings with UpdateSourceTrigger==LostFocus do not get updated when the user activates the menu or the toolbar.
This leads to the unfortunate situation that the last change that the user made gets lost when the user selects "Save File" from the menu or toolbar.
Is there an easy way around this or do I have to change all my bindings to UpdateSourceTrigger=PropertyChanged.
I know this is a bit old, but for any future reader, simply setting the following on my ToolBar worked for me:
FocusManager.IsFocusScope="False"
The problem is that the TextBox does, in fact, not lose focus when the menu item is activated. Thus, the UpdateSourceTrigger LostFocus does not fire. Depending on your (view)model, UpdateSourceTrigger PropertyChanged might or might not be a feasible workaround.
For me, PropertyChanged was not an option (I need to validate the data after the user finished entering it, not in between), so I used a workaround by calling this method before "Save File" (or any other menu/toolbar entry that requires an up-to-date model):
Public Shared Sub SaveFocusedTextBox()
Dim focusedTextBox = TryCast(Keyboard.FocusedElement, TextBox)
If focusedTextBox IsNot Nothing Then
Dim be = focusedTextBox.GetBindingExpression(TextBox.TextProperty)
If be IsNot Nothing Then be.UpdateSource()
End If
End Sub
A few other approaches for this problem can be found in this related question:
WPF Databind Before Saving
(In fact, credit for this method goes to rudigrobler's answer in that question.)
This works well for me:
Private Sub MenuItem_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
Keyboard.FocusedElement.RaiseEvent(New RoutedEventArgs With {.RoutedEvent = LostFocusEvent})
End Sub
While there are useful answers here, IMHO none are really the best way. For me, the best and second-best options are:
Fix the tab order so that there are focusable elements in the same focus scope as the TextBox both before and after the TextBox in tab order. This is IMHO the best option, but it should be used only when this is a reasonable user interface choice. I.e. there already are UI elements that would naturally surround the TextBox.
Handle the LostKeyboardFocus event and explicitly update the binding then. Note that while the TextBox hasn't lost focus in terms of the focus scope, it does always lose the keyboard focus if you tab out of it.
The second option looks like this:
<TextBox Text="{Binding SomeProperty}" LostKeyboardFocus="TextBox_LostKeyboardFocus"/>
private void TextBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
BindingOperations.GetBindingExpression((DependencyObject)sender, TextBox.TextProperty)?.UpdateSource();
}
To elaborate:
The accepted answer defers updating the bound source property until some specific scenario (such as executing a "Save" command) that is known to need the property value occurs. Unfortunately, this negates one key benefits of the MVVM paradigm, which is one doesn't need to worry about when things happen per se, since the binding engine is supposed to take care of everything.
The highest-voted answer improves on this slightly, but as noted in the comments below it, it's still a scenario-specific solution. It would need to be applied for every element in the UI that has its own focus scope. But even worse is that it actually modifies the UI behavior for an element that is otherwise completely unrelated to the element we actually care about. Good coding practices mean fixing the original problem, rather than applying some unrelated change that just happens to have a side-effect that works in our favor.
If one can simply arrange the tab order in the UI such that there is an element in the same focus scope as the TextBox that immediately follows the TextBox, then IMHO that'd be the ideal solution. It means that the UI works predictably for the user and the code is aligned with the simplest implementation possible.
But this may not always be possible. In some cases, the natural tab order demands that the TextBox is immediately preceded or followed by a Menu, ToolBar, or other element that is its own focus scope. In such cases, to me the most direct approach is simply to modify the "focus lost" behavior for the binding by explicitly handling the LostKeyboardFocus event and updating the binding source when that event happens.
I adapted #Heinzi's solution for C# and combined it with the IsKeyboardFocusWithinChanged event on my textbox. I was pulling my hair out until this saved the day.
XAML:
<TextBox
IsKeyboardFocusWithinChanged="UIElement_OnIsKeyboardFocusWithinChanged"
...
/>
And code behind:
private TextBox focusedTextBox;
private void UIElement_OnIsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if( e.NewValue is bool && (bool)e.NewValue == true )
focusedTextBox = sender as TextBox;
}
public void SaveFocusedTextBox()
{
var be = focusedTextBox?.GetBindingExpression(TextBox.TextProperty);
be?.UpdateSource();
}

Is there an event that triggers if the number of ListViewItems in a ListView changes? (Windows Forms)

I'd like to enable/disable some other controls based on how many items are in my ListView control. I can't find any event that would do this, either on the ListView itself or on the ListViewItemCollection. Maybe there's a way to generically watch any collection in C# for changes?
I'd be happy with other events too, even ones that sometimes fire when the items don't change, but for example the ControlAdded and Layout events didn't work :(.
#Domenic
Not too sure, Never quite got that far in the thought process.
Another solution might be to extend ListView, and when adding and removing stuff, instead of calling .items.add, and items.remove, you call your other functions. It would still be possible to add and remove without events being raised, but with a little code review to make sure .items.add and .items.remove weren't called directly, it could work out quite well. Here's a little example. I only showed 1 Add function, but there are 6 you would have to implement, if you wanted to have use of all the available add functions. There's also .AddRange, and .Clear that you might want to take a look at.
Public Class MonitoredListView
Inherits ListView
Public Event ItemAdded()
Public Event ItemRemoved()
Public Sub New()
MyBase.New()
End Sub
Public Function AddItem(ByVal Text As String) As ListViewItem
RaiseEvent ItemAdded()
MyBase.Items.Add(Text)
End Function
Public Sub RemoveItem(ByVal Item As ListViewItem)
RaiseEvent ItemRemoved()
MyBase.Items.Remove(Item)
End Sub
End Class
I can't find any events that you could use. Perhaps you could subclass ListViewItemCollection, and raise your own event when something is added, with code similar to this.
Public Class MyListViewItemCollection
Inherits ListView.ListViewItemCollection
Public Event ItemAdded(ByVal Item As ListViewItem)
Sub New(ByVal owner As ListView)
MyBase.New(owner)
End Sub
Public Overrides Function Add(ByVal value As System.Windows.Forms.ListViewItem) As System.Windows.Forms.ListViewItem
Dim Item As ListViewItem
Item = MyBase.Add(value)
RaiseEvent ItemAdded(Item)
Return Item
End Function
End Class
I think the best thing that you can do here is to subclass ListView and provide the events that you want.

Resources