When I expand items in my treeview so that scrolling is necessary, a scrollbar appears. However, it doesn't scroll down for the newly expanded branch of items - they get cropped by the bottom of the control. So as I continue expanding items at the bottom of the tree, I have to keep manually scrolling down to see the new children. Anyone have a suggestion for how make it automatically scroll to show the newly expanded items?
You can use a simple EventSetter in TreeViewItem style to invoke an event handler when the item is selected. Then call BringIntoView for the item.
<TreeView >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="Selected" Handler="TreeViewSelectedItemChanged" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
private void TreeViewSelectedItemChanged(object sender, RoutedEventArgs e)
{
TreeViewItem item = sender as TreeViewItem;
if (item != null)
{
item.BringIntoView();
e.Handled = true;
}
}
On the TreeView, handle the TreeViewItem.Expanded event (you can do this at the TreeView level because of event bubbling). In the Expanded handler, call BringIntoView on the TreeViewItem that raised the event.
You may need a bit of trial and error to get hold of the TreeViewItem in your event handler code. I think (haven't checked) that the sender argument to your Expanded event handler will be the TreeView (since that's where the event handler is attached) rather than the TreeViewItem. And the e.Source or e.OriginalSource may be an element in the TreeViewItem's data template. So you may need to use VisualTreeHelper to walk up the visual tree to find the TreeViewItem. But if you use the debugger to inspect the sender and the RoutedEventArgs this should be trivial to figure out.
(If you're able to get this working and want to bundle it up so you don't have to attach the same event handler to every TreeView, it should be easy to encapsulate it as an attached behaviour which will allow you to apply it declaratively, including via a Style.)
Use a dependency property on an IsSelected trigger:
<Style TargetType="{x:Type TreeViewItem}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="commands:TreeViewItemBehavior.BringIntoViewWhenSelected" Value="True" />
</Trigger>
</Style.Triggers>
Here's the code for the dependency property:
public static bool GetBringIntoViewWhenSelected(TreeViewItem treeViewItem)
{
return (bool)treeViewItem.GetValue(BringIntoViewWhenSelectedProperty);
}
public static void SetBringIntoViewWhenSelected(TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(BringIntoViewWhenSelectedProperty, value);
}
public static readonly DependencyProperty BringIntoViewWhenSelectedProperty =
DependencyProperty.RegisterAttached("BringIntoViewWhenSelected", typeof(bool),
typeof(TreeViewItemBehavior), new UIPropertyMetadata(false, OnBringIntoViewWhenSelectedChanged));
static void OnBringIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
TreeViewItem item = depObj as TreeViewItem;
if (item == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
item.BringIntoView();
}
Thanks to itowlson's answer, here's the expanded event handler code that works for both of my trees
private static void Tree_Expanded(object sender, RoutedEventArgs e)
{
// ignore checking, assume original source is treeviewitem
var treeViewItem = (TreeViewItem)e.OriginalSource;
var count = VisualTreeHelper.GetChildrenCount(treeViewItem);
for (int i = count - 1; i >= 0; --i)
{
var childItem = VisualTreeHelper.GetChild(treeViewItem, i);
((FrameworkElement)childItem).BringIntoView();
}
// do NOT call BringIntoView on the actual treeviewitem - this negates everything
//treeViewItem.BringIntoView();
}
I modified Jared's answer in combination with the strategy from here: https://stackoverflow.com/a/42238409/2477582
The main advantage is that there aren't n calls of BringIntoView() for n childs. There is only one call of BringIntoView for an area that covers all of the child's heights.
Additionally, the purpose of the referred topic is realized as well. But this part may be removed, if unwanted.
/// <summary>Prevents automatic horizontal scrolling, while preserving automatic vertical scrolling and other side effects</summary>
/// <remarks>Source: https://stackoverflow.com/a/42238409/2477582 </remarks>
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
// Ignore re-entrant calls
if (m_SuppressRequestBringIntoView)
return;
// Cancel the current scroll attempt
e.Handled = true;
// Call BringIntoView using a rectangle that extends into "negative space" to the left of our
// actual control. This allows the vertical scrolling behaviour to operate without adversely
// affecting the current horizontal scroll position.
m_SuppressRequestBringIntoView = true;
try
{
TreeViewItem tvi = sender as TreeViewItem;
if (tvi != null)
{
// take care of children
int ll_ChildCount = VisualTreeHelper.GetChildrenCount(tvi);
double ll_Height = tvi.ActualHeight;
if (ll_ChildCount > 0)
{
FrameworkElement ll_LastChild = VisualTreeHelper.GetChild(tvi, ll_ChildCount - 1) as FrameworkElement;
ll_Height += ll_ChildCount * ll_LastChild.ActualHeight;
}
Rect newTargetRect = new Rect(-1000, 0, tvi.ActualWidth + 1000, ll_Height);
tvi.BringIntoView(newTargetRect);
}
}
catch (Exception ex)
{
m_Log.Debug("Error in TreeViewItem_RequestBringIntoView: " + ex.ToString());
}
m_SuppressRequestBringIntoView = false;
}
The above solution works together with this:
/// <summary>Correctly handle programmatically selected items (needed due to the custom implementation of TreeViewItem_RequestBringIntoView)</summary>
/// <remarks>Source: https://stackoverflow.com/a/42238409/2477582 </remarks>
private void TreeViewItem_Selected(object sender, RoutedEventArgs e)
{
((TreeViewItem)sender).BringIntoView();
e.Handled = true;
}
This part takes care of toggling the elements at each click:
/// <summary>Support for single click toggle</summary>
private void TreeViewItem_MouseUp(object sender, MouseButtonEventArgs e)
{
TreeViewItem tvi = null;
// Source may be TreeViewItem directly, or be a ContentPresenter
if (e.Source is TreeViewItem)
{
tvi = e.Source as TreeViewItem;
}
else if (e.Source is ContentPresenter)
{
tvi = (e.Source as ContentPresenter).TemplatedParent as TreeViewItem;
}
if (tvi == null || e.Handled) return;
tvi.IsExpanded = !tvi.IsExpanded;
e.Handled = true;
}
Finally the XAML part:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView" />
<EventSetter Event="Selected" Handler="TreeViewItem_Selected" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
A simple event listener on the tree worked for me:
<TreeView Margin="10,40,10,10" Grid.Column="0" x:Name="treeView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" SelectedItemChanged="TreeView_SelectedItemChanged" />
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) {
if (e.NewValue == null)
return;
((TreeViewItem)e.NewValue).BringIntoView();
}
Related
I have a usercontrol that has a scrollviewer, then a bunch of child controls like text boxes, radio buttons, and listboxes, etc inside of it. I can use the mouse wheel to scroll the parent scrollviewer until my mouse lands inside a listbox then, the mouse wheel events start going to the listbox.
Is there any way to have the listbox send those events back up to the parent control? Removing the listbox from within side the parent control like this question suggests (Mouse wheel not working when over ScrollViewer's child controls) isnt a solution.
I have tried
private void ListBox_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
}
but that didnt work either.
Thanks
This can be accomplished via attached behaviors.
http://josheinstein.com/blog/index.php/2010/08/wpf-nested-scrollviewer-listbox-scrolling/
Edit:
Here is the linked solution:
"So instead I came up with the following IgnoreMouseWheelBehavior. Technically it’s not ignoring the MouseWheel, but it is “forwarding” the event back up and out of the ListBox. Check it."
/// <summary>
/// Captures and eats MouseWheel events so that a nested ListBox does not
/// prevent an outer scrollable control from scrolling.
/// </summary>
public sealed class IgnoreMouseWheelBehavior : Behavior<UIElement>
{
protected override void OnAttached( )
{
base.OnAttached( );
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel ;
}
protected override void OnDetaching( )
{
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
base.OnDetaching( );
}
void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice,e.Timestamp,e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
And here’s how you would use it in XAML.
<ScrollViewer Name="IScroll">
<ListBox Name="IDont">
<i:Interaction.Behaviors>
<local:IgnoreMouseWheelBehavior />
</i:Interaction.Behaviors>
</ListBox>
</ScrollViewer>
Where the i namespace is:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
The answer you have referenced is exactly what is causing your problem, the ListBox (which is composed of among other things a ScrollViewer) inside your ScrollViewer catches the MouseWheel event and handles it, preventing it from bubbling and thus the ScrollViewer has no idea the event ever occurred.
Use the following extremely simple ControlTemplate for your ListBox to demonstrate (note it does not have a ScrollViewer in it and so the MouseWheel event will not be caught) The ScrollViewer will still scroll with the mouse over the ListBox.
<UserControl.Resources>
<ControlTemplate x:Key="NoScroll">
<ItemsPresenter></ItemsPresenter>
</ControlTemplate>
</UserControl.Resources>
<ScrollViewer>
<SomeContainerControl>
<.... what ever other controls are inside your ScrollViewer>
<ListBox Template="{StaticResource NoScroll}"></ListBox>
<SomeContainerControl>
</ScrollViewer>
You do have the option of capturing the mouse when it enters the ScrollViewer though so it continues to receive all mouse events until the mouse is released, however this option would require you to delgate any further mouse events to the controls contained within the ScrollViewer if you want a response...the following MouseEnter MouseLeave event handlers will be sufficient.
private void ScrollViewerMouseEnter(object sender, MouseEventArgs e)
{
((ScrollViewer)sender).CaptureMouse();
}
private void ScrollViewerMouseLeave(object sender, MouseEventArgs e)
{
((ScrollViewer)sender).ReleaseMouseCapture();
}
Neither of the workarounds I have provided are really preferred however and I would suggest rethinking what you are actually trying to do. If you explain what you are trying to achieve in your question I'm sure you will get some more suggestions...
I followed Amanduh's approach to solve the same problem I had with multiple datagrids in a scrollviewer but in WPF:
public sealed class IgnoreMouseWheelBehavior
{
public static bool GetIgnoreMouseWheel(DataGrid gridItem)
{
return (bool)gridItem.GetValue(IgnoreMouseWheelProperty);
}
public static void SetIgnoreMouseWheel(DataGrid gridItem, bool value)
{
gridItem.SetValue(IgnoreMouseWheelProperty, value);
}
public static readonly DependencyProperty IgnoreMouseWheelProperty =
DependencyProperty.RegisterAttached("IgnoreMouseWheel", typeof(bool),
typeof(IgnoreMouseWheelBehavior), new UIPropertyMetadata(false, OnIgnoreMouseWheelChanged));
static void OnIgnoreMouseWheelChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var item = depObj as DataGrid;
if (item == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
item.PreviewMouseWheel += OnPreviewMouseWheel;
else
item.PreviewMouseWheel -= OnPreviewMouseWheel;
}
static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{RoutedEvent = UIElement.MouseWheelEvent};
var gv = sender as DataGrid;
if (gv != null) gv.RaiseEvent(e2);
}
}
As Simon said, it's the ScrollViewer in the standard ListBox template that's catching the event. To bypass it you can provide your own template.
<ControlTemplate x:Key="NoWheelScrollListBoxTemplate" TargetType="ListBox">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" Padding="1,1,1,1" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}" Name="Bd" SnapsToDevicePixels="True">
<!-- This is the new control -->
<l:NoWheelScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</l:NoWheelScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="UIElement.IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Panel.Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
</Trigger>
<Trigger Property="ItemsControl.IsGrouping" Value="True">
<Setter Property="ScrollViewer.CanContentScroll" Value="False" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
And the implementation for NoWheelScrollViewer is pretty simple.
public class NoWheelScrollViewer : ScrollViewer
{
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
// Do nothing
}
}
Then, whenever you want a listbox to not handle the mouse wheel.
<ListBox Template="{StaticResource NoWheelScrollListBoxTemplate}">
I was trying to adapt Simon Fox's answer for a DataGrid. I found the the template hid my headers, and I never got the mouseLeave event by doing it in C#. This is ultimately what worked for me:
private void DataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
((DataGrid)sender).CaptureMouse();
}
private void DataGrid_MouseWheel(object sender, MouseWheelEventArgs e)
{
((DataGrid)sender).ReleaseMouseCapture();
}
A simple solution which worked for me is to override the inner control template to remove the scroll viewer (whichever required) like this
For example
I have a structure like this
ListView (a)
ListView (b)
ListView (c)
I wanted to bubble the mouse wheel scroll of (b) to (a), however wanted to keep the mouse wheel scroll of (c) available.
I simply overridden the Template of (b) like this. This allowed me to bubble contents of (b) except (c) to (a). Also, I can still scroll the contents of (c). If i want to remove even for (c) then i have to repeat the same step.
<ListView.Template>
<ControlTemplate>
<ItemsPresenter />
</ControlTemplate>
</ListView.Template>
A modified Simon Fox's solution if the original doesn't work:
public sealed class IgnoreMouseWheelBehavior : Behavior<UIElement>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
}
protected override void OnDetaching()
{
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
base.OnDetaching();
}
static void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (!(sender is DependencyObject))
{
return;
}
DependencyObject parent = VisualTreeHelper.GetParent((DependencyObject) sender);
if (!(parent is UIElement))
{
return;
}
((UIElement) parent).RaiseEvent(
new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = UIElement.MouseWheelEvent });
e.Handled = true;
}
}
You must listening PreviewMouseWheel from ScrollViewer (it works), but not from listbox.
Whenever a node is selected in my treeview, it automatically does a horizontal scroll to that item. Is there a way to disable this?
Handle the RequestBringIntoView event and set Handled to true, and the framework won't try to bring the item into view. For example, do something like this in your XAML:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
And then this in your code-behind:
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true;
}
I managed to solve the problem using the following:
<TreeView ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ContentPresenter, AncestorLevel=1}}" />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
</TreeView>
I bind the width of the StackPanel which renders the ItemsPanel here, to the ActualWidth of ContentPresenter in the TreeView.
It also works nice with the "hacked" Stretching TreeView by: http://blogs.msdn.com/b/jpricket/archive/2008/08/05/wpf-a-stretching-treeview.aspx (I modified that solution not to remove grid column, but to change Grid.Column property of the first Decorator element from 1 to 2).
To offer a slightly simplified version of #lena's answer:
To scroll vertically while preserving the horizontal scroll position, and with no unwanted side effects, in the XAML, add event handlers for RequestBringIntoView and Selected:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView"/>
<EventSetter Event="Selected" Handler="OnSelected"/>
...
In the code behind, add two event handlers:
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
// Ignore re-entrant calls
if (mSuppressRequestBringIntoView)
return;
// Cancel the current scroll attempt
e.Handled = true;
// Call BringIntoView using a rectangle that extends into "negative space" to the left of our
// actual control. This allows the vertical scrolling behaviour to operate without adversely
// affecting the current horizontal scroll position.
mSuppressRequestBringIntoView = true;
TreeViewItem tvi = sender as TreeViewItem;
if (tvi != null)
{
Rect newTargetRect = new Rect(-1000, 0, tvi.ActualWidth + 1000, tvi.ActualHeight);
tvi.BringIntoView(newTargetRect);
}
mSuppressRequestBringIntoView = false;
}
private bool mSuppressRequestBringIntoView;
// Correctly handle programmatically selected items
private void OnSelected(object sender, RoutedEventArgs e)
{
((TreeViewItem)sender).BringIntoView();
e.Handled = true;
}
Matthew, I manged to preserve vertical scrolling, and only prevent horizontal scrolling by restoring the horizontal position after a scroll caused by the RequestBringIntoView event .
private double treeViewHorizScrollPos = 0.0;
private bool treeViewResetHorizScroll = false;
private ScrollViewer treeViewScrollViewer = null;
private void TreeViewItemRequestBringIntoView( object sender, RequestBringIntoViewEventArgs e )
{
if ( this.treeViewScrollViewer == null )
{
this.treeViewScrollViewer = this.DetailsTree.Template.FindName( "_tv_scrollviewer_", this.DetailsTree ) as ScrollViewer;
if( this.treeViewScrollViewer != null )
this.treeViewScrollViewer.ScrollChanged += new ScrollChangedEventHandler( this.TreeViewScrollViewerScrollChanged );
}
this.treeViewResetHorizScroll = true;
this.treeViewHorizScrollPos = this.treeViewScrollViewer.HorizontalOffset;
}
private void TreeViewScrollViewerScrollChanged( object sender, ScrollChangedEventArgs e )
{
if ( this.treeViewResetHorizScroll )
this.treeViewScrollViewer.ScrollToHorizontalOffset( this.treeViewHorizScrollPos );
this.treeViewResetHorizScroll = false;
}
I had a similar problem. I needed to prevent horizontal scroll but preserve vertical scroll. My solution is to handle OnRequestBringIntoView method as I want it to behave. I created a ResourceDictionary for a TreeViewItem and added EventSetters for OnSelected and OnRequestBringIntoView methods.
MyResourceDictionary.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" x:Class="Resources.MyResourceDictionary" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="TreeViewItem" x:Key="treeitem" >
<EventSetter Event="RequestBringIntoView" Handler="OnRequestBringIntoView"/>
<EventSetter Event="Selected" Handler="OnSelected"/>
</Style>
</ResourceDictionary>
MyResourceDictionary.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Resources
{
partial class MyResourceDictionary:ResourceDictionary
{
public MyResourceDictionary()
{
InitializeComponent();
}
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true; //prevent event bubbling
var item = (TreeViewItem)sender;
TreeView tree = GetParentTree(item) as TreeView;
if(tree!=null)
{
var scrollViewer = tree.Template.FindName("_tv_scrollviewer_", tree) as ScrollViewer;
if (scrollViewer != null)
{
scrollViewer.ScrollToLeftEnd();//prevent horizontal scroll
Point relativePoint = item.TransformToAncestor(tree).Transform(new Point(0, 0));//get position of a selected item
if (relativePoint.Y <= scrollViewer.ContentVerticalOffset) return;//do no scroll if we select inside one 'scroll screen'
scrollViewer.ScrollToVerticalOffset(relativePoint.Y);//scroll to Y of a selected item
}
}
}
private DependencyObject GetParentTree(DependencyObject item)
{
var target = VisualTreeHelper.GetParent(item);
return target as TreeView != null ? target : GetParentTree(target);
}
private void OnSelected(object sender, RoutedEventArgs e) //handle programmatically selected items
{
var item = (TreeViewItem)sender;
item.BringIntoView();
e.Handled = true;
}
}
}
Following solution is more simple and fully tested and more compatible, You don't need to calculate and change scrollbar offset, what you need is moving horizontal scrollbar to left, since "RequestBringIntoView" event routing strategy is bubbling, you simply need to do it on last item reached event.
Name scrollViewer control "_tv_scrollviewer_"
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView"/>
On code behind:
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
var item = (TreeViewItem)sender;
if (item != null)
{
// move horizontal scrollbar only when event reached last parent item
if (item.Parent == null)
{
var scrollViewer = itemsTree.Template.FindName("_tv_scrollviewer_", itemsTree) as ScrollViewer;
if (scrollViewer != null)
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, (Action)(() => scrollViewer.ScrollToLeftEnd()));
}
}
}
#lena's solution of preserving vertical scrolling worked best for me. I've iterated on it a little bit:
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
var treeViewItem = (TreeViewItem)sender;
var scrollViewer = treeView.Template.FindName("_tv_scrollviewer_", treeView) as ScrollViewer;
Point topLeftInTreeViewCoordinates = treeViewItem.TransformToAncestor(treeView).Transform(new Point(0, 0));
var treeViewItemTop = topLeftInTreeViewCoordinates.Y;
if (treeViewItemTop < 0
|| treeViewItemTop + treeViewItem.ActualHeight > scrollViewer.ViewportHeight
|| treeViewItem.ActualHeight > scrollViewer.ViewportHeight)
{
// if the item is not visible or too "tall", don't do anything; let them scroll it into view
return;
}
// if the item is already fully within the viewport vertically, disallow horizontal scrolling
e.Handled = true;
}
What this does is let the ScrollViewer scroll normally if the item isn't already in the viewport vertically. However for the actual "annoying" case (where the item is already visible), it sets e.Handled to true, thus preventing horizontal scrolling.
I had a DataGrid that I wanted to do the same operation on and used POHB's answer mostly. I had to modify it for my solution. The code is shown below. The datagrid is a 2 x 2 datagrid with the first column being thin and the second being very wide (1000+). The first column is frozen. I hope this helps someone out. -Matt
public partial class MyUserControl : UserControl
{
private ScrollContentPresenter _scrollContentPresenter;
private ScrollViewer _scrollViewer;
private double _dataGridHorizScrollPos = 0.0;
private bool _dataGridResetHorizScroll = false;
public MyUserControl()
{
// setup code...
_dataGrid.ApplyTemplate();
_scrollViewer = FindVisualChild<ScrollViewer>(_dataGrid);
_scrollViewer.ScrollChanged += new ScrollChangedEventHandler(DataGridScrollViewerScrollChanged);
_scrollContentPresenter = FindVisualChild<ScrollContentPresenter>(_scrollViewer);
_scrollContentPresenter.RequestBringIntoView += new RequestBringIntoViewEventHandler(_scrollContentPresenter_RequestBringInputView);
}
private void DataGridScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_dataGridResetHorizScroll)
{
_scrollViewer.ScrollToHorizontalOffset(_dataGridHorizScrollPos);
}
// Note: When the row just before a page change is selected and then the next row on the
// next page is selected, a second event fires setting the horizontal offset to 0
// I'm ignoring those large changes by only recording the offset when it's large. -MRB
else if (Math.Abs(e.HorizontalChange) < 100)
{
_dataGridHorizScrollPos = _scrollViewer.HorizontalOffset;
}
_dataGridResetHorizScroll = false;
}
public T FindVisualChild<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if ((child != null) && (child is ScrollViewer))
{
// I needed this since the template wasn't applied yet when
// calling from the constructor
(child as ScrollViewer).ApplyTemplate();
}
if (child != null && child is T)
{
return (T)child;
}
T childItem = FindVisualChild<T>(child);
if (childItem != null) return childItem;
}
}
return null;
}
private void _scrollContentPresenter_RequestBringInputView(object sender, RequestBringIntoViewEventArgs e)
{
_dataGridResetHorizScroll = true;
}
I want to execute some code when a a selected row of the WPF DataGrid is double clicked. I know that the datagrid has a MouseDoubleClicked event and that it also has a row selected event but I don't see any event for "selected row double clicked" ...
Do you think it's possible to capture this event somehow ?
you can add the event handler in the ItemContainerStyle (which is the style applied to a row) :
<DataGrid ... >
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDoubleClick" Handler="Row_DoubleClick"/>
</Style>
</DataGrid.ItemContainerStyle>
...
</DataGrid>
Then, in the handler, you can check if the row is selected
private void Row_DoubleClick(object sender, MouseButtonEventArgs e)
{
// execute some code
}
This question came up for me while looking for a solution and the answers didn't work, whether due to age or my own implementation. Either way, here is the solution that worked for me.
Add the MouseDoubleClick event to the DataGrid
<DataGrid x:Name="DatagridMovie"
Width="Auto"
CanUserAddRows="False"
CanUserDeleteRows="True"
IsReadOnly="true"
ItemsSource="{Binding}"
MouseDoubleClick="Row_MouseDoubleClick">
and in the method
private void Row_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
// Ensure row was clicked and not empty space
var row = ItemsControl.ContainerFromElement((DataGrid)sender,
e.OriginalSource as DependencyObject) as DataGridRow;
if ( row == null ) return;
… Stuff();
}
So far I haven't noticed any problems with it. It doesn't share the problem that others have that means double clicking a header or empty space with a row selected beforehand would still cause it to run.
With data binding and MVVM you would do one-click event (=selectedItem of row) like this:
<Datagrid ItemsSource="{Binding YourObservableCollectionProperty}"
SelectedItem="{Binding YourSelectedItemProperty}">
//more...
</Datagrid>
CodeBehind:
public partial class YourClass : Window
{
public YourClass()
{
InitializeComponent();
this.DataContext = new YourClassViewModel();
}
}
ViewModel:
public class YourClassViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private ObservableCollection<YourModelClass> _yourObservableCollectionProperty;
public ObservableCollection<YourModelClass> YourObservableCollectionProperty
{
get { return _yourObservableCollectionProperty; }
set
{
_yourObservableCollectionProperty = value;
OnPropertyChanged("YourObservableCollectionProperty");
}
}
private YourModelClass _yourSelectedItemProperty;
public YourModelClass YourSelectedItemProperty
{
get { return _yourSelectedItemProperty; }
set
{
_yourSelectedItemProperty = value;
OnPropertyChanged("YourSelectedItemProperty");
}
}
//Constructor
public YourClassViewModel()
{
/*Take your ModelClass instance and ObservableCollection instance here
and play around with them or move them into a method. Normally your
observablecollection is the itemssource of your datagrid and your selecteditem
is your modelclass.*/
}
}
You could try current cell changed event handler it works only with one click and not double click if thats what your looking for, since double click can be used to for initiating editing cell or entire row or for any other process:
private void datagrid_CurrentCellChanged(object sender, EventArgs e)
{
int selected_index = datagrid.SelectedIndex + 1;
// this is used for debugging and testing.
//MessageBox.Show("The index of the row for the clicked cell is " + selected_index);
}
The ItemContainerStyle do not have best solution, suggest use the RowStyle:
In your XAML:
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDoubleClick" Handler="DataGridRow_MouseDoubleClick"/>
</Style>
</DataGrid.RowStyle>
In your Code:
private void DataGridRow_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
//your logic here
}
Why don't you get the SelectedRow property while the DoubleClick event happens and do something with it? If the SelectedRow is null, it means no Row is selected so just return
private void Grid_DoubleClick(object sender, RoutedEventArgs e)
{
if(grid.SelectedRow == null)
return; // return if there's no row selected
// do something with the Selected row here
}
Use rowstyle and MouseDoubleClick work, like Darlan Dieterich said.
But when there are button or checkbox or other controls in cell, they will handle event but not prevent event passing to row, cause weird behavior. Use MouseDown maybe better in these case.
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDown" Handler="DataGridRow_MouseDown"/>
</Style>
</DataGrid.RowStyle>
private void DataGridRow_MouseDown(object sender, MouseButtonEventArgs e)
{
if(e.ClickCount != 2)
{
return;
}
// code here
e.Handled = true;
}
I've almost got this working apart from one little annoying thing...
Because the ListBox selection happens on mouse down, if you start the drag with the mouse down when selecting the last item to drag it works fine, but if you select all the items to drag first and then click on the selection to start dragging it, the one you click on gets unselected and left behind after the drag.
Any thoughts on the best way to get around this?
<DockPanel LastChildFill="True">
<ListBox ItemsSource="{Binding SourceItems}"
SelectionMode="Multiple"
PreviewMouseLeftButtonDown="HandleLeftButtonDown"
PreviewMouseLeftButtonUp="HandleLeftButtonUp"
PreviewMouseMove="HandleMouseMove"
MultiSelectListboxDragDrop:ListBoxExtension.SelectedItemsSource="{Binding SelectedItems}"/>
<ListBox ItemsSource="{Binding DestinationItems}"
AllowDrop="True"
Drop="DropOnToDestination"/>
<DockPanel>
...
public partial class Window1
{
private bool clickedOnSourceItem;
public Window1()
{
InitializeComponent();
DataContext = new WindowViewModel();
}
private void DropOnToDestination(object sender, DragEventArgs e)
{
var viewModel = (WindowViewModel)
e.Data.GetData(typeof(WindowViewModel));
viewModel.CopySelectedItems();
}
private void HandleLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var sourceElement = (FrameworkElement)sender;
var hitItem = sourceElement.InputHitTest(e.GetPosition(sourceElement))
as FrameworkElement;
if(hitItem != null)
{
clickedOnSourceItem = true;
}
}
private void HandleLeftButtonUp(object sender, MouseButtonEventArgs e)
{
clickedOnSourceItem = false;
}
private void HandleMouseMove(object sender, MouseEventArgs e)
{
if(clickedOnSourceItem)
{
var sourceItems = (FrameworkElement)sender;
var viewModel = (WindowViewModel)DataContext;
DragDrop.DoDragDrop(sourceItems, viewModel, DragDropEffects.Move);
clickedOnSourceItem = false;
}
}
}
I've found a very simple way to enable Windows Explorer like drag/drop behaviour when having multiple items selected. The solution replaces the common ListBox with a little derived shim that replaces the ListBoxItem with a more intelligent version. This way, we can encapsulate the click state at the right level and call into the protected selection machinery of the ListBox. Here is the relevant class. For a complete example, see my repo on github.
public class ListBoxEx : ListBox
{
protected override DependencyObject GetContainerForItemOverride()
{
return new ListBoxItemEx();
}
class ListBoxItemEx : ListBoxItem
{
private bool _deferSelection = false;
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (e.ClickCount == 1 && IsSelected)
{
// the user may start a drag by clicking into selected items
// delay destroying the selection to the Up event
_deferSelection = true;
}
else
{
base.OnMouseLeftButtonDown(e);
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (_deferSelection)
{
try
{
base.OnMouseLeftButtonDown(e);
}
finally
{
_deferSelection = false;
}
}
base.OnMouseLeftButtonUp(e);
}
protected override void OnMouseLeave(MouseEventArgs e)
{
// abort deferred Down
_deferSelection = false;
base.OnMouseLeave(e);
}
}
}
So...having become the proud owner of a tumbleweed badge, I've got back on to this to try & find a way around it. ;-)
I'm not sure I like the solution so I'm still very much open to any better approaches.
Basically, what I ended up doing is remember what ListBoxItem was last clicked on & then make sure that gets added to the selected items before a drag. This also meant looking at how far the mouse moves before starting a drag - because clicking on a selected item to unselect it could sometimes result in it getting selected again if mouse bounce started a little drag operation.
Finally, I added some hot tracking to the listbox items so, if you mouse down on a selected item it'll get unselected but you still get some feedback to indicate that it will get included in the drag operation.
private void HandleLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var source = (FrameworkElement)sender;
var hitItem = source.InputHitTest(e.GetPosition(source)) as FrameworkElement;
hitListBoxItem = hitItem.FindVisualParent<ListBoxItem>();
origPos = e.GetPosition(null);
}
private void HandleLeftButtonUp(object sender, MouseButtonEventArgs e)
{
hitListBoxItem = null;
}
private void HandleMouseMove(object sender, MouseEventArgs e)
{
if (ShouldStartDrag(e))
{
hitListBoxItem.IsSelected = true;
var sourceItems = (FrameworkElement)sender;
var viewModel = (WindowViewModel)DataContext;
DragDrop.DoDragDrop(sourceItems, viewModel, DragDropEffects.Move);
hitListBoxItem = null;
}
}
private bool ShouldStartDrag(MouseEventArgs e)
{
if (hitListBoxItem == null)
return false;
var curPos = e.GetPosition(null);
return
Math.Abs(curPos.Y-origPos.Y) > SystemParameters.MinimumVerticalDragDistance ||
Math.Abs(curPos.X-origPos.X) > SystemParameters.MinimumHorizontalDragDistance;
}
XAML changes to include hot tracking...
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<Border Background="{TemplateBinding Background}" />
<Border Background="#BEFFFFFF" Margin="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition /><RowDefinition />
</Grid.RowDefinitions>
<Border Margin="1" Grid.Row="0" Background="#57FFFFFF" />
</Grid>
</Border>
<ContentPresenter Margin="8,5" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="PowderBlue" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#5FB0E0E6" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
One option would be not to allow ListBox or ListView to remove selected items until MouseLeftButtonUp is triggered.
Sample code:
List<object> removedItems = new List<object>();
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count > 0)
{
ListBox box = sender as ListBox;
if (removedItems.Contains(e.RemovedItems[0]) == false)
{
foreach (object item in e.RemovedItems)
{
box.SelectedItems.Add(item);
removedItems.Add(item);
}
}
}
}
private void ListBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (removedItems.Count > 0)
{
ListBox box = sender as ListBox;
foreach (object item in removedItems)
{
box.SelectedItems.Remove(item);
}
removedItems.Clear();
}
}
I'm surprised that the behaviour difference between ListBox and the Windows Explorer has not been taken care of after 4 years across 3 major updates of the .NET framework.
I ran in to this problem back in Silverlight 3. I ended up overriding the mouse down and mouse up event handler to fully simulate the Windows Explorer behaviour.
I don't have the source code any more but the logic should be:
When mouse down
if the target item is not selected, clear existing selection
if Ctrl key is down, add target item to selection
if Shift key is down
if there is a previously selected item, add all items between target item and previous item to selection
else only add target item to selection
if the target item is selected de-select only if Ctrl key is down
When mouse up (on the same item)
if the target item is selected
if Ctrl key is down, remove item from selection
if Shift key is down
if there is a previously selected item, remove all items between target item and previous item from selection
else only remove target item from selection
However
This should really be Microsoft's job to update the behaviour to be consistent to the operating system and to be more intuitive. I've submitted it as a bug to Microsoft if any body wants to vote for it: http://connect.microsoft.com/VisualStudio/feedback/details/809192/
I had a similar problem. I Started with the basic implementation from https://www.c-sharpcorner.com/uploadfile/dpatra/drag-and-drop-item-in-listbox-in-wpf/
and Modified to something like this:
ListBox dragSource = null;
ObservableCollection<String> dragItems;
private void ListBox_Drop(object sender, DragEventArgs e)
{
ListBox parent = (ListBox)sender;
// check if the Items are from an different source
if(dragSource != parent)
{
// Add and remove the Items of both sources
foreach (var item in dragItems)
{
((ObservableCollection<String>)dragSource.ItemsSource).Remove(item);
((ObservableCollection<String>)parent.ItemsSource).Add(item);
}
}
}
private void ListBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Check if Modifiers for Selection modes are pressed
if(Keyboard.Modifiers != ModifierKeys.Control && Keyboard.Modifiers != ModifierKeys.Shift)
{
ListBox parent = (ListBox)sender;
dragSource = parent;
object data = GetDataFromListBox(dragSource, e.GetPosition(parent));
dragItems = new ObservableCollection<String>();
for(int i = 0; i < parent.SelectedItems.Count; i++)
{
dragItems.Add(parent.SelectedItems[i] as String);
}
//If the Data is currently selected drop whole selection
if(dragItems.Contains(data as String))
{
DragDrop.DoDragDrop(parent, parent.SelectedItems, DragDropEffects.Move);
}
// The data is not selected, so clear selection and try to drop the current Item
else
{
dragItems.Clear();
dragItems.Add(data as String);
parent.SelectedItems.Clear();
DragDrop.DoDragDrop(parent, data, DragDropEffects.Move);
}
}
}
private static object GetDataFromListBox(ListBox source, Point point)
{
UIElement element = source.InputHitTest(point) as UIElement;
if (element != null)
{
object data = DependencyProperty.UnsetValue;
while (data == DependencyProperty.UnsetValue)
{
data = source.ItemContainerGenerator.ItemFromContainer(element);
if (data == DependencyProperty.UnsetValue)
{
element = VisualTreeHelper.GetParent(element) as UIElement;
}
if (element == source)
{
return null;
}
}
if (data != DependencyProperty.UnsetValue)
{
return data;
}
}
return null;
}
Hope this helps anyone to sumples across this Thread
I understand why this is happening. The bounding box of a parent TreeViewItem includes its children's bounding boxes, so when I am moused over a TreeViewItem, all its parents in the tree are also moused over. Is there something besides IsMouseOver I should be using?
http://blogs.msdn.com/mikehillberg/archive/2006/09/21/MyTreeViewHelperIsMouseDirectlyOverItem.aspx
this link solved the problem, I did not try the Original Source idea.
<Style TargetType="TreeViewItem">
<Style.Triggers>
<Trigger Property="local:MyTreeViewHelper.IsMouseDirectlyOverItem" Value="True">
<Setter Property="Background" Value="Green" />
</Trigger>
</Style.Triggers>
</Style>
where local:MyTreeViewHelper.IsMouseDirectlyOverItem is attached property
public static class MyTreeViewHelper
{
//
// The TreeViewItem that the mouse is currently directly over (or null).
//
private static TreeViewItem _currentItem = null;
//
// IsMouseDirectlyOverItem: A DependencyProperty that will be true only on the
// TreeViewItem that the mouse is directly over. I.e., this won't be set on that
// parent item.
//
// This is the only public member, and is read-only.
//
// The property key (since this is a read-only DP)
private static readonly DependencyPropertyKey IsMouseDirectlyOverItemKey =
DependencyProperty.RegisterAttachedReadOnly("IsMouseDirectlyOverItem",
typeof(bool),
typeof(MyTreeViewHelper),
new FrameworkPropertyMetadata(null, new CoerceValueCallback(CalculateIsMouseDirectlyOverItem)));
// The DP itself
public static readonly DependencyProperty IsMouseDirectlyOverItemProperty =
IsMouseDirectlyOverItemKey.DependencyProperty;
// A strongly-typed getter for the property.
public static bool GetIsMouseDirectlyOverItem(DependencyObject obj)
{
return (bool)obj.GetValue(IsMouseDirectlyOverItemProperty);
}
// A coercion method for the property
private static object CalculateIsMouseDirectlyOverItem(DependencyObject item, object value)
{
// This method is called when the IsMouseDirectlyOver property is being calculated
// for a TreeViewItem.
if (item == _currentItem)
return true;
else
return false;
}
//
// UpdateOverItem: A private RoutedEvent used to find the nearest encapsulating
// TreeViewItem to the mouse's current position.
//
private static readonly RoutedEvent UpdateOverItemEvent = EventManager.RegisterRoutedEvent(
"UpdateOverItem", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyTreeViewHelper));
//
// Class constructor
//
static MyTreeViewHelper()
{
// Get all Mouse enter/leave events for TreeViewItem.
EventManager.RegisterClassHandler(typeof(TreeViewItem), TreeViewItem.MouseEnterEvent, new MouseEventHandler(OnMouseTransition), true);
EventManager.RegisterClassHandler(typeof(TreeViewItem), TreeViewItem.MouseLeaveEvent, new MouseEventHandler(OnMouseTransition), true);
// Listen for the UpdateOverItemEvent on all TreeViewItem's.
EventManager.RegisterClassHandler(typeof(TreeViewItem), UpdateOverItemEvent, new RoutedEventHandler(OnUpdateOverItem));
}
//
// OnUpdateOverItem: This method is a listener for the UpdateOverItemEvent. When it is received,
// it means that the sender is the closest TreeViewItem to the mouse (closest in the sense of the tree,
// not geographically).
static void OnUpdateOverItem(object sender, RoutedEventArgs args)
{
// Mark this object as the tree view item over which the mouse
// is currently positioned.
_currentItem = sender as TreeViewItem;
// Tell that item to re-calculate the IsMouseDirectlyOverItem property
_currentItem.InvalidateProperty(IsMouseDirectlyOverItemProperty);
// Prevent this event from notifying other tree view items higher in the tree.
args.Handled = true;
}
//
// OnMouseTransition: This method is a listener for both the MouseEnter event and
// the MouseLeave event on TreeViewItems. It updates the _currentItem, and updates
// the IsMouseDirectlyOverItem property on the previous TreeViewItem and the new
// TreeViewItem.
static void OnMouseTransition(object sender, MouseEventArgs args)
{
lock (IsMouseDirectlyOverItemProperty)
{
if (_currentItem != null)
{
// Tell the item that previously had the mouse that it no longer does.
DependencyObject oldItem = _currentItem;
_currentItem = null;
oldItem.InvalidateProperty(IsMouseDirectlyOverItemProperty);
}
// Get the element that is currently under the mouse.
IInputElement currentPosition = Mouse.DirectlyOver;
// See if the mouse is still over something (any element, not just a tree view item).
if (currentPosition != null)
{
// Yes, the mouse is over something.
// Raise an event from that point. If a TreeViewItem is anywhere above this point
// in the tree, it will receive this event and update _currentItem.
RoutedEventArgs newItemArgs = new RoutedEventArgs(UpdateOverItemEvent);
currentPosition.RaiseEvent(newItemArgs);
}
}
}
}
This link provided a great solution that worked for me.
Effectively, you can override the ControlTemplate and specify the SourceName for the IsMouseOver Setter to just the header portion of the TreeViewItem. So yes, you are technically moused over both the child and parent TreeViewItems, but the trigger only fires for the item where the cursor is over the header.
Ok - I had your problem and i've wasted almost a day solving it...
All you need to do is to take care of that from the code behind.
For example:
In XAML:
<TreeViewItem Header="{Binding diskName}"
Background="Transparent" Mouse.MouseEnter="changeBackground">
And in the cs file:
private void changeBackground(object sender, MouseEventArgs e)
{
TreeViewItem t = (TreeViewItem)sender;
t.Background = (SolidColorBrush)(new BrushConverter().ConvertFrom("#CCE8FF"));
e.Handled = false;
}
That should do it. Good luck!
That sounds like the right event. One thing you can do to prevent MouseOver events is to check the MouseOver events for your TreeViewItems. If your RoutedEventArgs.OriginalSource does not equal the parent, just return and manage your event handlers manually.
public void TreeViewItem_MouseOver(object sender, RoutedEventArgs e)
{
if (sender != e.OriginalSource) return;
}