I have some issue and I'll appreciate any help on it!
I have a window with dynamic count of listboxes. MVVM was used. For listbox and it's functionality a usercontrol with viewModel was created. So when window is creating, I create my specific count of userControls in the constructor of the window.
And actually the problem.
Listbox has it's MaxWidth=350. And each listboxItem styled with buttons in the end.
When names are short - everything OK and user can see buttons in the listbox exactly after window was loaded.
But if names are too long, the listBox creates a horizontal scrollBar because of MaxWidth=350 (expected behavior.) So once the form is loaded, it seems like the buttons were not created in some listBoxes (see image2:)Image2: scrollBar appeared and buttons are not visible
So the question is: how can I programmatically move scrollbar before the window will be shown? Image 3 shows what I need, and image 2 shows the current view of the listbox. Needed result after loading the window
Do you know how to move scrollBar to the end?
Thank you, #Yog, for your tip with Focus(). I indeed didn't realize that Focus method can move scrollBar!
And I remembered the method, that I had already used in c#, I found that method in this site: How can I find WPF controls by name or type?
So I just rewrote this method to vb.net as an Extension method, called it on ListBox, found stackPanel with buttons and Focus() that stackPanel!
IMPORTANT: if somebody wants to figure out his problem with Focus() method, make sure that your specific xaml element has set Focusable=true
Here is my method on Vb.NET:
Public Function FindChild(Of T As DependencyObject)(parent As DependencyObject, childName As String) As T
If parent Is Nothing Then Return Nothing
Dim foundChild As T = Nothing
Dim childrenCount As Integer = VisualTreeHelper.GetChildrenCount(parent)
For i = 0 To childrenCount
Dim child As DependencyObject
Try
child = VisualTreeHelper.GetChild(parent, i)
Catch ex As Exception
Continue For
End Try
'If the child is not of the request child type child
Dim childType As T = TryCast(child, T)
If childType Is Nothing Then
'recursively drill down the tree
foundChild = child.FindChild(Of T)(childName)
'If the child is found, break so we do not overwrite the found child.
If foundChild IsNot Nothing Then Exit For
Else
If Not String.IsNullOrEmpty(childName) Then
Dim frameworkElement = TryCast(child, FrameworkElement)
'If the child's name is set for search
If frameworkElement IsNot Nothing AndAlso frameworkElement.Name = childName Then
'if the child's name is of the request name
foundChild = TryCast(child, T)
Exit For
End If
Else
'child element found.
foundChild = TryCast(child, T)
Exit For
End If
End If
Next
Return foundChild
End Function
VisualTreeHelper.GetChild method threw an exception, I didn't figure out why, so I just put it to try-catch.
Related
I have a Button on a UserControl that adds an item to a ListBox on that UserControl. Let's call that control Parent. The ListBoxItems contain another UserControl. Let's call that Child. The button adds an item to the ItemSource of the listbox (MVVM style).
I can scroll that into view without a problem. I can set the focus to the ListBoxItem, but what I want is the focus to be set on the first TextBox of the child UserControlof the content of the ListBoxItem. I can't seem to figure that out. The code below sets the focus to the ListBoxItem, not the UserControl child of it or any control on it.
Private Sub bnAdd(sender As Object, e As RoutedEventArgs)
VM.AddDetail()
MyList.ScrollIntoView(MyList.Items(MyList.Items.Count - 1))
Dim ListBoxItem As ListBoxItem = MyList.ItemContainerGenerator.ContainerFromItem(MyList.SelectedItem)
ListBoxItem.Focus()
End Sub
On my child UserControl I used this in XAML:
FocusManager.FocusedElement="{Binding ElementName=txtMyBox}"
There is a related question here and most of the approaches use hooking into focus events to achieve the focus change. I want to propose another solution that is based on traversing the visual tree. Unfortunately, I can only provide you C# code, but you can use the concept to apply it in your Visual Basic code.
As far as I can see, you are using code-behind to scroll your items into view. I will build on that. Your list box has list box items and I guess you use a data template to display UserControls as child items. Within these user controls there is a TextBox that you have assigned a name in XAML via x:Name or in code-behind via the Name property. Now, you need a helper method to traverse the visual tree and search for text boxes.
private IEnumerable<TextBox> FindTextBox(DependencyObject dependencyObject)
{
// No other child controls, break
if (dependencyObject == null)
yield break;
// Search children of the current control
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
// Check if the current item is a text box
if (child is TextBox textBox)
yield return textBox;
// If we did not find a text box, search the children of this child recursively
foreach (var childOfChild in FindTextBox(child))
yield return childOfChild;
}
}
Then we add a method that filters the enumerable of text boxes for a given name using Linq.
private TextBox FindTextBox(DependencyObject dependencyObject, string name)
{
// Filter the list of text boxes for the right one with the specified name
return FindTextBox(dependencyObject).SingleOrDefault(child => child.Name.Equals(name));
}
In your bnAdd handler you can take your ListBoxItem, search for the text box child and focus it.
var textBox = FindTextBox(listBoxItem, "MyTextBox");
textBox.Focus();
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>
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)
I have a navigation control that is displayed in multiple pages. So, I have no way to identify the exact class name during design time. Now, when user navigates to different page, I want to hide the current page. Basically, a typical menubar behaviour. I am able to get the outermost element as a dependency object using the code below.
Private Function GetTopLevelControl(ByVal control As DependencyObject) As DependencyObject
Dim tmp As New DependencyObject
tmp = control
Dim parent As New DependencyObject
parent = Nothing
While Not VisualTreeHelper.GetParent(tmp) Is Nothing
parent = VisualTreeHelper.GetParent(tmp)
End While
Return parent
End Function
Now, on the mouse down event, I am trying to write code to hide this parent object.
Private Sub Menu_Nomination_MouseDown(sender As Object, e As MouseButtonEventArgs)
Dim surveySearchPage As New SurveySearch
surveySearchPage.Show()
Dim parentControl As DependencyObject
parentControl = GetTopLevelControl(Me)
parentControl
End Sub
Problem is parentControl object has no hide or close property at all. So, I am currently stuck in trying to close the page.
I've been working with Bea's solution here for a while and finding it very helpful. Problem now I'm having is when I drag-n-drop items within or to another ListView control and I want to scroll up/down "during" the drag (moving an item from index 30 to index 1), it's not happening. I would have to drag to the top of the visual items in the ListView, manually scroll up, then drag again, eventually ending at the position I want. This isn't very user friendly.
Now I found the function (DragDropHelper.DropTarget_PreviewDragOver) that I would want to do the testing of which item is being dragged over, and I'm getting that.
Dim pt As Point = e.GetPosition(DirectCast(Me.targetItemsControl, UIElement))
' Perform the hit test against a given portion of the visual object tree.
Dim result As HitTestResult = VisualTreeHelper.HitTest(Me.targetItemsControl, pt)
Now from there I can get the DependencyProperty of this visual hit
Dim lvi As ListViewItem = TryCast(GetDependencyObjectFromVisualTree(TryCast(result.VisualHit, DependencyObject), GetType(ListViewItem)), ListViewItem)
Which is of a ListViewItem. Now in the function DropTarget_PreviewDragOver I have the "DraggedItem" which is of type Picture in Bea's example, but that can change depending on the ObservableCollection you have bound to the ListView. Now, I want to drag the ListView up or down depending on where the mouse is on the control. I've attempted with the below un-finished non-working code
If lvi IsNot Nothing Then
If pt.Y <= 25 Then
Dim lv As ListView = TryCast(targetItemsControl, ListView)
If lv IsNot Nothing Then
Dim index As Integer = lv.Items.IndexOf(lvi)
If index > 1 Then
lv.ScrollIntoView(lv.Items(index - 1))
End If
End If
Else
If pt.Y >= Me.targetItemsControl.ActualHeight - 25 Then
Debug.Print("Scroll Down")
End If
End If
End If
Can someone point me in the right direction to get this ItemsControl or ListView to scroll when dragging over the items??
Thanks!
I'm still messing around with this exact same issue too. I'm using a slightly modified version of Bea's Drag and Drop found here, which is in VB instead of C#. When I used ScrollIntoView as described above, I could scroll down but not up. So I messed around and came up with this as my DropTarget_PreviewDragOver:
Private Sub DropTarget_PreviewDragOver(ByVal sender As Object, ByVal e As DragEventArgs)
Dim draggedItem As Object = e.Data.GetData(Me.m_format.Name)
Me.DecideDropTarget(e)
If (Not draggedItem Is Nothing) Then
If (TypeOf m_targetItemsControl Is ListBox) Then
Dim lb As ListBox = CType(m_targetItemsControl, ListBox)
Dim temp As Integer = m_insertionIndex
Dim scroll As ScrollViewer = Utilities.GetScrollViewer(lb)
If scroll.VerticalOffset = temp Then
temp -= 1
End If
If temp >= 0 And temp <= (lb.Items.Count - 1) Then
lb.ScrollIntoView(lb.Items(temp))
End If
End If
Me.ShowDraggedAdorner(e.GetPosition(Me.m_topWindow))
Me.UpdateInsertionAdornerPosition()
End If
e.Handled = True
End Sub
and I had to include this utility function, taken from here
Public Shared Function GetScrollViewer(ByVal listBox As ListBox)
Dim scroll_border As Decorator = CType(VisualTreeHelper.GetChild(listBox, 0), Decorator)
If (TypeOf scroll_border Is Decorator) Then
Dim scroll As ScrollViewer = CType(scroll_border.Child, ScrollViewer)
If (TypeOf scroll Is ScrollViewer) Then
Return scroll
Else
Return Nothing
End If
Else
Return Nothing
End If
End Function
which is great and all. Then running out what theuberk mentioned above with the adorner moving, and in the spirit of making this easy for someone else later, I added a variable to the DragDropAdorner class:
Private m_mouseDelta As Point
Added this to the last line of DragSource_PreviewMouseLeftButtonDown:
Me.m_mouseDelta = e.GetPosition(m_sourceItemContainer)
And turned ShowDraggedAdorner into:
Private Sub ShowDraggedAdorner(ByVal currentPosition As Point)
If (Me.m_draggedAdorner Is Nothing) Then
Dim adornerLayer As AdornerLayer = adornerLayer.GetAdornerLayer(Me.m_topWindow.Content)
Me.m_draggedAdorner = New DraggedAdorner(Me.m_draggedData, DragDropBehavior.GetDragTemplate(Me.m_sourceItemsControl), m_topWindow.Content, adornerLayer)
End If
Me.m_draggedAdorner.SetPosition((currentPosition.X - m_mouseDelta.X), (currentPosition.Y - m_mouseDelta.Y))
End Sub
What I did was took advantage of the ListBox.ScrollIntoView method. Basically, when you update your drop target, you can just call this method on it and wpf will do all the work. All you need to know is the index of the drop target item. This handles both vertical and horizontal scrolling.
this.listView.ScrollIntoView(this.listView.Items[index]);
When you use this method, your adorner might move with the scrolling ListBox. To fix this, I just set my adorner parent and adorner layer parent to the content of the window at the top of the visual tree (i.e. topWindow.Content).
Another possibility to scroll is to use the ScrollBar-Commands. You can do this without climbing down the VisualTree. If you have a styles ListBox with no border the GetScrollViewer()-Method would not work anymore.
I use the first Element of the ItemContainerGenerator as CommandTarget for the ScrollBar.LineXXXCommand:
Point p = e.GetPosition(itemsControl);
IInputElement commandTarget = itemsControl.ItemContainerGenerator.ContainerFromIndex(0) as IInputElement;
if (commandTarget != null)
{
if (p.Y < OFFSET_TO_SCROLL)
ScrollBar.LineUpCommand.Execute(null, commandTarget);
else if (p.Y > itemsControl.ActualHeight - OFFSET_TO_SCROLL)
ScrollBar.LineDownCommand.Execute(null, commandTarget);
if (p.X < OFFSET_TO_SCROLL)
ScrollBar.LineLeftCommand.Execute(null, commandTarget);
else if (p.X > itemsControl.ActualWidth - OFFSET_TO_SCROLL)
ScrollBar.LineRightCommand.Execute(null, commandTarget);
}
Calling the LineXXXCommands is similiar to clicking the Arrow-Buttons of a ScrollBar: The ScrollViewer scrolles by a certain ammount which you can configure by setting the 'SmallAmount' Property of the ScrollBar.