WPF ListView Databound Drag/Drop Auto Scroll - wpf

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.

Related

Move scrollbar in listbox programmatically exactly after window was loaded

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.

WPF Losing caret position when moving mouse over adorner in richtextbox

I have an adorner layer in my richtextbox. Whenever a word is misspelled I add an adorner to the richtextbox's adornerlayer. I want to be able to right click on the adorner and capture that event but I don't want to lose the caretposition of the richtextbox when the mouse is over the adorned element.
I've tried setting IsHitTestVisible=False. This keeps the mouse from changing to a pointer when I'm over the adorned element like I want so that I can click on the underlying richtextbox, but it complicates the rightmousebutton click capturing.
Protected Sub Editor_PreviewRightMouseButtonUp(ByVal sender As Object, ByVal e As MouseEventArgs)
' Retreive the coordinates of the mouse button event.
Dim pt As Point = e.GetPosition(CType(sender, UIElement))
' Initiate the hit test by setting up a hit test result callback method.
VisualTreeHelper.HitTest(Me, Nothing, New HitTestResultCallback(AddressOf HitTestCallBack_PreviewRightMouseButtonUp), New PointHitTestParameters(pt))
e.Handled = True
End Sub
Public Function HitTestCallBack_PreviewRightMouseButtonUp(ByVal result As HitTestResult) As HitTestResultBehavior
If result.VisualHit.GetType() Is GetType(HighLightAdorner) Then
Dim adornerControl As HighLightAdorner = DirectCast(result.VisualHit, HighLightAdorner)
Dim e = New MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Right)
e.RoutedEvent = Mouse.PreviewMouseUpEvent
adornerControl.RaiseEvent(e)
Return HitTestResultBehavior.Stop
End If
Return HitTestResultBehavior.Continue
End Function
This allows me to capture the rightmousebutton click but it only seems to work when I click right at the bottom of the adornedElement. If I try to click in the body of the adornedElement it doesn't find it in the hitTesting.
Am I going about this correctly or is there a better way to allow an underlying control to maintain it cursor when an element above it needs to be able to accept mouse events.
Thanks for the help!

WPF refresh TreeView when it loses the focus

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
I have find this solution (but I don't like it very much). The problem is depending from the added items that wpf, for some reasons, doesn't remember that exist. Then I do a "manual" refresh with a method that clear and re-add all the items in the source-collection:
Public Sub RefreshData(ByVal RicambiListPass As ObservableCollection(Of P_RicambiItem))
Dim l As New List(Of P_RicambiItem)
l.AddRange(RicambiListPass)
_RicambiList.Clear()
For Each i As P_RicambiItem In l
_RicambiList.Add(i)
Next
End Sub

WinForms ListView empty rows glitch

I have a glitch in a WinForms C# ListView (with custom modifications to sort and filters on all columns, but it happened also in a standard ListView).
I modify the ListView Items with this (fairly standard) pattern:
BeginUpdate();
// add some items
// remove some other items
Sort();
EndUpdate();
But if I call this code when the ListView is already scrolled, then I get some empty (non selectable) rows before the real items, and 2 scrollabars even if they are not needed.
It looks like a graphic glitch, because when I scroll the list then the empty items disappear.
Have anyone met this problem before?
Ok, I found the problem. A call to set a column Width = -2 during Resize was messing the owner-draw filters...
This ListView graphic bug. A similar problem can be reproduced, if while changing the ListView size, change the width of its columns. As a solution, the method proposes to change the width in a separate thread.
private void ListView_SizeChanged(object sender, EventArgs e)
{
var widthChangedThread = new Thread(() => SetNewColumnSize()) {IsBackground = true};
widthChangedThread.Start();
}
private void SetNewColumnSize()
{
Invoke(new MethodInvoker(() =>_columnHeader.Width += 10));
}
This control behave in a strange way but setting up the scrollabe
property to false in the resize event fixed completly the problem as:
With DirectCast(sender, ListView)
'do not allow scrolling in the resize event
'ortherwise there is a condition where the control
'stop showing the row data
IsScrollEnabled = .Scrollable
.Scrollable = False
ThisColumnHeader = .Columns("colMessage")
If ThisColumnHeader IsNot Nothing Then
.BeginUpdate()
'With .Columns("colMessage")
' .Width = -2
'End With
If .Items.Count > 0 Then
'If MyListDownloadMessage.Count > 0 Then
If ToolStripSerialRxFillDown.Checked Then
.EnsureVisible(.Items.Count - 1)
Else
If .TopItem IsNot Nothing Then
.EnsureVisible(.TopItem.Index)
End If
End If
End If
.EndUpdate()
.Refresh()
End If
.Scrollable = IsScrollEnabled
End With

WPF Drag-to-scroll doesn't work correctly

I am tying to realize a drag-to-scroll functionality in my application and have problems on my way. Can anybody help me?
I have a ScrollViewer and inside it an ItemsControl and within ItemsTemplate I have a UserControl. I want to drag that UserControl within ItemsControl. I want the ScrollViewer to scroll down, when I am dragging to the boundaries of the ItemsControl.
protected override void OnPreviewMouseMove(System.Windows.Input.MouseEventArgs e)
{
if (this.IsMouseCaptured)
{
// Get the new mouse position.
Point mouseDragCurrentPoint = e.GetPosition(this);
if (Math.Abs(mouseDragCurrentPoint.Y - this.ActualHeight) <= 50)
{
this._scrollStartOffset.Y += 5;
_containingScrollViewer.ScrollToVerticalOffset(this._scrollStartOffset.Y);
}
if (mouseDragCurrentPoint.Y <= 50)
{
this._scrollStartOffset.Y -= 5;
_containingScrollViewer.ScrollToVerticalOffset(this._scrollStartOffset.Y);
}
}
base.OnPreviewMouseMove(e);
}
When I start dragging by calling DragDrop.DoDragDrop() scrolling don't happens. But when I disable dragging, the ScrollViewer scrolls down dependong on mouse position.
Maybe there's something that I don't take into accont about dragging and Capturing the mouse?
Thanks for attention.
Garegin
When using DragDrop.DoDragDrop(), I use a Sub that Handles the Me.DragOver event (in VB) so it looks as follows. Mind you, my control has a ListBox wrapped in a ScrollViewer.
Private Sub ListBox_Items_DragOver(ByVal sender As System.Object, ByVal e As System.Windows.DragEventArgs) Handles Me.DragOver
Dim currentMousePoint As Point = e.GetPosition(_containtingScrollViewer)
If Math.Abs(currentMousePoint.Y - _containtingScrollViewer.ActualHeight) <= 50 Then
_containtingScrollViewer.ScrollToVerticalOffset(currentMousePoint.Y + 5)
End If
If currentMousePoint.Y <= 50 Then
_containtingScrollViewer.ScrollToVerticalOffset(currentMousePoint.Y - 5)
End If
End Sub
This gives me the ability to scroll whilst dragging items. You can tweak the tolerances to get better/smoother scrolling as needed.

Resources