MouseDoubleClick gets wrong TreeViewItem - wpf

This one is really weird unless I'm missing something really basic.
I have attached an event handler to my TreeViewItem's MouseDoubleClick event through ItemContainerStyle:
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<EventSetter Event="MouseDoubleClick" Handler="TreeViewItem_MouseDoubleClick" />
</Style>
</TreeView.ItemContainerStyle>
Here's the event handler:
Private Sub TreeViewItem_MouseDoubleClick(sender As Object, e As MouseButtonEventArgs)
If TypeOf sender Is TreeViewItem Then
Dim TVI = DirectCast(sender, TreeViewItem)
MsgBox(TVI.Header)
End If
End Sub
The problem is that MsgBox always shows the header text of the root node, not the node on which I double-clicked. I can't see any obvious mistake here. Can anyone point me in the right direction?

Yup, you're missing one of the basic weird things about a TreeView :). Not sure how I can illustrate it easily in a post, so I'll try to explain it. A TreeViewItem has sub items. When you expand the root TreeViewItem to show the sub items, all those sub TreeViewItems are inside of the parent TreeViewItem. So when you double click on a child TreeViewItem the event will bubble up to the top most item. If you look at e.OriginalSource, you'll get the actual item... sort of... you'll actually get the object you double clicked on (for example the TextBlock). You can use the well known GetVisualAncestor<T>() extension method to chase up to the correct TreeViewItem:
((FrameworkElement)e.OriginalSource).GetVisualAncestor<TreeViewItem>()
Yeah, it's ugly :)...
Here is a link to a C# implementation, you'll have to find a VB version for yourself :), but its pretty trivial to port.
https://code.google.com/p/gong-wpf-dragdrop/source/browse/branches/jon/GongSolutions.Wpf.DragDrop/Utilities/VisualTreeExtensions.cs?r=29

Related

Expand/Collapse TreeView Nodes on Ctrl+LeftMouse in 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.

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>

WPF refresh TreeView when it loses the focus

Because of the changes I have done to my post I have thinked to open another thread. In the new thread I have posted my (provvisory) solution.
You can find it here
Hi!
I have a problem with my TreeView in a WPF application (Framework 3.5 SP1).
It's a TreeVIew with 2 Levels of Data. I expand / collapse the items of the first level in a particular way (with a single mouse-click on the TreeViewItem). Again when I expand a first-level TreeViewItem, I add some second-level TreeViewItems to the group (it's an important detail, infact if I don't add the items the problem doesn't occur). All works good until the TreeView loses focus.
If, for example, I expand the TreeViewItem at the first position, adding at the same time one element to the second-level, then I click on a button (to let the TreeView lose the focus), and then I click again on the TreeViewItem at the third position to expand it, the TreeViewItem that results from the hit-test with the mouse position is not the "real" TreeViewItem (in this case the third), but a TreeViewItem which is in an higher position than the one clicked (in this case the second).
I have tried to use the UpdateLayout method on the TreeView-LostFocus event, but without results. Probably I need a method that does the opposite: starting from the UI, refresh the object that contains the position of the TreeViewItems.
Can you, please, help me?
Thank you!
Pileggi
This is the code:
' in this way I tried to put remedy at the problem, but it doesn't work.
Private Sub tvArt_LostFocus(ByVal sender As Object, ByVal e As RoutedEventArgs) Handles tvArt.LostFocus
Me.tvArt.UpdateLayout()
e.Handled = True
End Sub
' here I expand / collapse the items of the first level of my TreeView
Private Sub tvArt_PreviewMouseUp(ByVal sender As System.Object, ByVal e As MouseButtonEventArgs) Handles tvArt.PreviewMouseUp
Dim p As Point = Nothing
Dim tvi As TreeViewItem = getItemFromMousePosition(Of TreeViewItem)(p, e.OriginalSource, Me.tvArt)
If tvi Is Nothing = False Then
If tvi.HasItems Then
Dim be As BindingExpression = BindingOperations.GetBindingExpression(tvi, TreeViewItem.ItemsSourceProperty)
Dim ri As P_RicambiItem = DirectCast(be.DataItem, P_RicambiItem)
If ri.isExpanded = False then
' here I add items to the second level collection
End If
ri.isExpanded = Not ri.isExpanded
End If
End If
e.Handled = True
End Sub
Private Function getItemFromMousePosition(Of childItem As DependencyObject)(ByRef p As Point, ByVal sender As UIElement, _
ByVal _item As UIElement) As childItem
p = sender.TranslatePoint(New Point(0, 0), _item)
Dim obj As DependencyObject = DirectCast(_item.InputHitTest(p), DependencyObject)
While obj Is Nothing = False AndAlso TypeOf obj Is childItem = False
obj = VisualTreeHelper.GetParent(obj)
End While
Return DirectCast(obj, childItem)
End Function
Your hit test code seems a little odd. You ignore the mouse position given by the MouseButtonEventArgs object, and then do a hit test in the TreeView against the upper-left corner of the control that was clicked. This will normally give you back the same control again, and I suspect your weird behavior is in the cases where it doesn't. Instead of doing TranslatePoint and InputHitTest, just use the sender directly. Your helper function reduces to:
Private Function getParentOfType(Of childItem As DependencyObject)(ByVal sender As UIElement) As childItem
Dim obj As DependencyObject = sender
While obj Is Nothing = False AndAlso TypeOf obj Is childItem = False
obj = VisualTreeHelper.GetParent(obj)
End While
Return DirectCast(obj, childItem)
End Function
You can actually make it simpler again by taking advantage of the fact that MouseUp is a routed event and letting it find the TreeViewItem parent for you. Instead of adding the event handler to the TreeView itself, add a MouseUp handler to the TreeViewItem, and it will always be called with a sender of the TreeViewItem.
You should also set your binding on IsExpanded to be two-way if it is not already. That way you can update IsExpanded on the TreeViewItem and the value will be pushed to the binding source.
In XAML:
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding isExpanded, Mode=TwoWay}" />
<EventSetter Event="Mouse.MouseUp" Handler="tvi_MouseUp"/>
</Style>
</TreeView.ItemContainerStyle>
Then in code:
Private Sub tvi_MouseUp(ByVal sender As System.Object, ByVal e As MouseButtonEventArgs)
Dim tvi As TreeViewItem = DirectCast(sender, TreeViewItem)
If tvi.HasItems Then
tvi.IsExpanded = Not tvi.IsExpanded
End If
e.Handled = True
End Sub
Thank you, you are very kind. But unfortunately the problem is the same with your solution.
I omitted an important detail (sorry): when I expand a first-level TreeViewItem I add some second-level TreeviewItems. This causes the problem, if I don't add the items all works good.
I have edited my question, to make it more comprehensible.
Maybe now the solution is more easy (I hope).
Thanks,
Pileggi
Because of the changes I have done to my post I have thinked to open another thread. In the new thread I have posted my (provvisory) solution. You can find it here

WPF ListView ScrollViewer Double-Click Event

Doing the below will reproduce my problem:
New WPF Project
Add ListView
Name the listview: x:Name="lvList"
Add enough ListViewItems to the ListView to fill the list completely so a vertical scroll-bar appears during run-time.
Put this code in the lvList.MouseDoubleClick event
Debug.Print("Double-Click happened")
Run the application
Double-click on the LargeChange area of the scroll-bar (Not the scroll "bar" itself)
Notice the Immediate window printing the double-click happened message for the ListView
How do I change this behavior so MouseDoubleClick only happens when the mouse is "over" the ListViewItems and not when continually clicking the ScrollViewer to scroll down/up in the list?
You can't change the behaviour, because the MouseDoubleClick handler is attached to the ListView control, so it has to occur whenever the ListView is clicked -- anywhere. What you can do it detect which element of the ListView first detected the double-click, and figure out from there whether it was a ListViewItem or not. Here's a simple example (omitting error checking):
private void lv_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
DependencyObject src = (DependencyObject)(e.OriginalSource);
while (!(src is Control))
src = VisualTreeHelper.GetParent(src);
Debug.WriteLine("*** Double clicked on a " + src.GetType().Name);
}
Note the use of e.OriginalSource to find the actual element that was double-clicked. This will typically be something really low level like a Rectangle or TextBlock, so we use VisualTreeHelper to walk up to the containing control. In my trivial example, I've assumed that the first Control we hit will be the ListViewItem, which may not be the case if you're dealing with CellTemplates that contain e.g. text boxes or check boxes. But you can easily refine the test to look only for ListViewItems -- but in that case don't forget to handle the case there the click is outside any ListViewItem and the search eventually hits the ListView itself.
Maybe this helps?
Private Sub LstView_MouseDoubleClick(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles LstView.MouseDoubleClick
Dim source As FrameworkElement = TryCast(e.OriginalSource, FrameworkElement)
If IsNothing(source) Then Return
Dim TmplParent As DependencyObject = TryCast(source.TemplatedParent, DependencyObject)
If IsNothing(TmplParent) Then Return
If Not TmplParent.GetType.Equals(GetType(System.Windows.Controls.ListViewItem)) Then e.Handled = True
End Sub
I don't have VS handy to test if this works, but have you tried handling the double-click event on the ListViewItems rather than the ListView itself?
<ListView ListViewItem.MouseDoubleClick="lv_MouseDoubleClick" ... />
That should handle the MouseDoubleClick event on any child ListViewItem controls inside the ListView. Let us know if it works!
<Style TargetType="{x:Type ListViewItem}">
<EventSetter Event="MouseDoubleClick" Handler="OnListViewDoubleClick" />
</Style>
If you apply this style, it works. Just double click on item in the listview will work.
also, you have to remove the double click from the listview.

Setting User Control's DataContext from Code-Behind

This should be pretty easy, but it throws VS2008 for a serious loop.
I'm trying out WPF with MVVM, and am a total newbie at it although I've been developing for about 15 years, and have a comp. sci. degree. At the current client, I am required to use VB.Net.
I have renamed my own variables and removed some distractions in the code below, so please forgive me if it's not 100% syntactically perfect! You probably don't really need the code to understand the question, but I'm including it in case it helps.
I have a very simple MainView.xaml file:
<Window x:Class="MyApp.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Main Window" Height="400" Width="800" Name="MainWindow">
<Button Name="Button1">Show Grid</Button>
<StackPanel Name="teststack" Visibility="Hidden"/>
</Window>
I also have a UserControl called DataView that consists of a DataGrid:
<UserControl x:Class="MyApp.Views.DataView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfToolkit="http://schemas.microsoft.com/wpf/2008/toolkit" >
<Grid>
<WpfToolkit:DataGrid
ItemsSource="{Binding Path=Entries}" SelectionMode="Extended">
</WpfToolkit:DataGrid>
</Grid>
</UserControl>
The constructor for the DataView usercontrol sets up the DataContext by binding it to a view model, as shown here:
Partial Public Class DataView
Dim dataViewModel As ViewModels.DataViewModel
Public Sub New()
InitializeComponent()
dataViewModel = New ViewModels.DataViewModel
dataViewModel.LoadDataEntries()
DataContext = dataViewModel
End Sub
End Class
The view model for DataView looks like this (there isn't much in ViewModelBase):
Public Class DataViewModel
Inherits ViewModelBase
Public Sub New()
End Sub
Private _entries As ObservableCollection(Of DataEntryViewModel) = New ObservableCollection(Of DataEntryViewModel)
Public ReadOnly Property Entries() As ObservableCollection(Of DataEntryViewModel)
Get
Return _entries
End Get
End Property
Public Sub LoadDataEntries()
Dim dataEntryList As List(Of DataEntry) = DataEntry.LoadDataEntries()
For Each dataentry As Models.DataEntry In dataEntryList
_entries.Add(New DataEntryViewModel(dataentry))
Next
End Sub
End Class
Now, this UserControl works just fine if I instantiate it in XAML. When I run the code, the grid shows up and populates it just fine.
However, the grid takes a long time to load its data, and I want to create this user control programmatically after the button click rather than declaratively instantiating the grid in XAML. I want to instantiate the user control, and insert it as a child of the StackPanel control:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Dim dataView As New DataView
teststack.Children.Add(dataView)
End Sub
When I do this, as soon as the Button1_Click finishes, my application locks up, starts eating RAM, and hits the CPU about 50%.
Am I not instantiating my UserControl properly? It all seems to come down to the DataContext assignment in DataEntry's constructor. If I comment that out, the app works as expected (without anything in the grid, of course).
If I move this code block into Button1_Click (basically moving DataEntry's constructor code up a level), the app still fails:
dataViewModel = New ViewModels.DataViewModel
dataViewModel.LoadDataEntries()
dataView.DataContext = dataViewModel
I'm stumped. Can anybody give me some tips on what I could be doing wrong, or even how to debug what infinite loop my app is getting itself into?
Many thanks.
The root cause of your issue appears to be either the raw amount of data you're loading or some inefficiency in how you load that data. Having said that, the reason you're seeing the application lock up is that you're locking the UI thread when loading the data.
I believe that in your first case the data loading has been off loaded onto another thread to load the data. In you second example you're instantiating the control on the UI thread and as a result all the constructor and loading logic is performed on the current thread (the UI thread). If you offload this work onto another thread then you should see similar results to the first example.
I eventually gave up on trying to get the DataContext on the UserControl set during instantiation of the UserControl (either in XAML or code). Now I load up the data and set the DataContext of the UserControl in an event in the UserControl (IsVisibleChanged, I believe). When I instantiate the UserControl in XAML, I have it's Visibility set to Hidden. When Button1 is clicked, I set the UserControl's Visibility to Visible. So the UserControl pops into view, and it loads up its data and DataContext is set. Seems to work, but also seems very kludgey. :-( Thanks for the help, folks!
If it's only a matter of your control taking a long time to populate data, you should populate the control on another thread then add it through a delegate:
Since I'm not too good at writing VB.NET, but here's the C# equivalent:
private void Button1_Click(Object sender, RoutedEventArgs e)
{
Thread thr = new Thread(delegate()
{
DataView dataView = new DataView();
this.Dispatcher.BeginInvoke((Action) delegate()
{
teststack.Children.Add(dataView);
});
});
thr.Start();
}

Resources