WPF DataGrid: how do I stop auto scrolling when a cell is clicked?
Problem:
If myDataGrid
is not entirely visible (horizontal & vertical scrollbars are showing) and I click on one of my cells that is partially visible, the grid auto-scrolls to bring that cell into view. I don't want this to happen. I've tried playing around with RequestBringIntoView
, like this:
private void DataGrid_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled =开发者_JAVA百科 true;
}
But that does nothing.
Things I've tried:
- My cells are custom
UserControls
; I tried putting an event handler forRequestBringIntoView
on allUserControls
that make up my cells, and tried handling the event, thinking that maybe I wasn't doing enough by just handlingRequestBringIntoView
on theDataGrid
itself. This did not work. - Hosted the
DataGrid
inside of aScrollViewer
, and handled theScrollViewer
'sRequestBringIntoView
event. This actually works, and stops the auto-scrolling behavior, but in my case hosting aDataGrid
inside of aScrollViewer
is not at all desirable, so I need to come up with a different solution.
I'm not sure how to stop this behavior, any ideas?
Define an EventSetter
in the DataGrid.RowStyle
to call a handler that prevents the row from being brought into view:
XAML
<DataGrid>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<EventSetter Event="Control.RequestBringIntoView" Handler="DataGrid_Documents_RequestBringIntoView" />
</Style>
</DataGrid.RowStyle>
</DataGrid>
Handler
private void DataGrid_Documents_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true;
}
I took more time to have a look at this problem as my first solution wasn't working.
However the answer of John is almost the good one. The trick is to catch the RequestBringIntoView event BEFORE it gets to the ScrollViewer in order to mark it has handled.
If you don't have to refine the whole template, you can use the following code:
var scp = TreeHelper.FindVisualChild<ScrollContentPresenter>(this.datagrid);
scp.RequestBringIntoView += (s, e) => e.Handled = true;
We use the ScrollContentPresenter because it's just below the ScrollViewer in the visual tree.
Hope this helps !
You can access the DataGrid's internal ScrollViewer by modifying the template. Although normally you wouldn't put an event handler to code behind in a template, if you declare the template inline you can treat the event handler the same way you are when you attach it to the DataGrid itself. This is the default template as generated from Blend including an added handler on the ScrollViewer for the RequestBringIntoView event:
<ControlTemplate TargetType="{x:Type Controls:DataGrid}">
<Border SnapsToDevicePixels="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
<ScrollViewer x:Name="DG_ScrollViewer" Focusable="False" RequestBringIntoView="DG_ScrollViewer_RequestBringIntoView">
<ScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type Controls:DataGrid}}}" Focusable="False">
<Button.Visibility>
<Binding Path="HeadersVisibility" RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type Controls:DataGrid}}">
<Binding.ConverterParameter>
<Controls:DataGridHeadersVisibility>All</Controls:DataGridHeadersVisibility>
</Binding.ConverterParameter>
</Binding>
</Button.Visibility>
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Rectangle x:Name="Border" Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" SnapsToDevicePixels="True"/>
<Polygon x:Name="Arrow" Fill="Black" Stretch="Uniform" HorizontalAlignment="Right" Margin="8,8,3,3" VerticalAlignment="Bottom" Opacity="0.15" Points="0,10 10,10 10,0"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="Border" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="Border" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Visibility" TargetName="Arrow" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
<Button.Command>
<RoutedCommand/>
</Button.Command>
</Button>
<Custom:DataGridColumnHeadersPresenter x:Name="PART_ColumnHeadersPresenter" Grid.Column="1">
<Custom:DataGridColumnHeadersPresenter.Visibility>
<Binding Path="HeadersVisibility" RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type Controls:DataGrid}}">
<Binding.ConverterParameter>
<Controls:DataGridHeadersVisibility>Column</Controls:DataGridHeadersVisibility>
</Binding.ConverterParameter>
</Binding>
</Custom:DataGridColumnHeadersPresenter.Visibility>
</Custom:DataGridColumnHeadersPresenter>
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter" Grid.ColumnSpan="2" Grid.Row="1" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" ContentTemplate="{TemplateBinding ContentTemplate}" CanContentScroll="{TemplateBinding CanContentScroll}" CanHorizontallyScroll="False" CanVerticallyScroll="False"/>
<ScrollBar x:Name="PART_VerticalScrollBar" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Grid.Column="2" Grid.Row="1" Maximum="{TemplateBinding ScrollableHeight}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" Orientation="Vertical" ViewportSize="{TemplateBinding ViewportHeight}"/>
<Grid Grid.Column="1" Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding NonFrozenColumnsViewportHorizontalOffset, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type Controls:DataGrid}}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ScrollBar x:Name="PART_HorizontalScrollBar" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Grid.Column="1" Maximum="{TemplateBinding ScrollableWidth}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}"/>
</Grid>
</Grid>
</ControlTemplate>
</ScrollViewer.Template>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
I had the same problem as Rumit, but found a solution/hack.
I thought if I could find a way to differentiate between mouse clicks and arrow keys, then I could set e.Handled accordingly.
After some experimentation I found that e.OriginalSource changed depending on mouse or arrow key. For a mouse click, the handler for RequestBringIntoView is called once and e.OriginalSource was of type DataGridCell. For an arrow key, the handler is called twice and e.OriginalSource is of types DataGridRow and then DataGridCell.
The code for my handler is:
e.Handled = (e.OriginalSource is DataGridCell);
This seems like a bit of a hack, but works great for me.
I had the same problem and Jan's answer helped me. The only missing thing was that ScrollContentPresenter will be found only after Loaded event occurs. I created an extended DataGrid class inherited from DataGrid with additional property AutoScroll to control if I want the grid to scroll automatically or not.
Here's the class:
using System.Windows;
using System.Windows.Controls;
using Microsoft.Windows.Controls;
namespace Bartosz.Wojtowicz.Wpf
{
public class ExtendedDataGrid : DataGrid
{
public bool AutoScroll { get; set; }
public ExtendedDataGrid()
{
AutoScroll = true;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs eventArgs)
{
if (!AutoScroll)
{
ScrollContentPresenter scp = DataGridHelper.GetVisualChild<ScrollContentPresenter>(this);
if (scp != null) scp.RequestBringIntoView += OnRequestBringIntoView;
}
}
private static void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true;
}
}
}
And here's how you use it:
<local:ExtendedDataGrid AutoScroll="False">
<!-- your grid definition -->
</local:ExtendedDataGrid>
The l33t way:
static App()
{
EventManager.RegisterClassHandler(typeof(ScrollContentPresenter),
FrameworkElement.RequestBringIntoViewEvent,
new RoutedEventHandler(OnRequestBringIntoView));
}
private static void OnRequestBringIntoView(object sender, RoutedEventArgs e)
{
e.Handled = true;
}
Note that this could interfere with e.g. third-party controls.
I'm not sure this is working but here is an idea based on some investigation is made in the DataGrid's source code using Reflector:
1/ create a class which inherits DataGridCellsPanel. This is the Panel that is used internally by the DataGrid in order to arrange the cells
2/ override the BringIndexIntoView method by an empty method (without calling the base method)
3/ set the ItemsPanelTemplate property in your XAML:
<tk:DataGrid>
<tk:DataGrid.ItemsPanel>
<ItemsPanelTemplate>
<local:DataGridCellsPanelNoAutoScroll />
</ItemsPanelTemplate>
</tk:DataGrid.ItemsPanel>
</tk:DataGrid>
It seems that when a MouseDown event occurs, at some point the BringIndexIntoView method of the panel is called to do the auto-scroll. Replacing it with a no-op might do the trick.
I hadn't time to test this solution, please let us know if it's working.
Here's what worked for me (after trying all of the less complex "answers" to date):
<DataGrid Grid.Column="0" Grid.Row="1"
Name="ListItemContainerDataGrid"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.CanContentScroll="False"
And.Others
ItemsSource="{Binding Path=ListItemModels}"
>
</DataGrid>
ScrollViewer.CanContentScroll="False" seems mindbogglingly counter intuitive...
As Dr.WPF has answered a similar question here the RequestBringIntoView should be handled in ItemsPanel.
精彩评论