Click here to Skip to main content
15,881,812 members
Articles / Desktop Programming / WPF
Tip/Trick

Prevent WPF DataGrid Auto-scrolling Due to Clicking in a Wide Column

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
24 Aug 2019CC (ASA 2.5)4 min read 10.8K   137   3   1
In full-row selection mode, the WPF DataGrid wants to scroll horizontally when a user clicks on a cell which extends offscreen. This sample project shows one way to prevent it.

Introduction

WPF has many rough edges which make it difficult for beginners to use--and which often require projects to include mountains of work-around code to do things that should be trivial. This is one such work-around, blessedly short, designed to fix the specific problem where you have a DataGrid containing columns extending offscreen, and you don't want the grid's horizontal scroll position to change because the user clicked on the "wrong" cell in order to select the full row.

My Solution

The crux of the solution is to find and keep a reference to the ScrollViewer which manages the DataGrid's scroll position, and attach to it a handler which will be called when the scroll position changes. When that handler is called because a horizontal scroll happened immediately after a mouse click in a cell, the handler can suppress the undesirable behavior by resetting the ScrollViewer's horizontal offset.

This solution is designed to allow normal scrolling with the horizontal scrollbar or keyboard. Horizontal scrolling is only suppressed when it happens very soon after a mouse button is clicked in one of the grid's cells.

I don't know that this is the very best solution to the problem, only that I'm not comfortable with solutions which begin with "Edit the control's template and...." Beyond that, my web researches turned up many possible solutions, but none of the ones I tried seemed to work very well.

Using the Code

The first part of this solution is in your application's XAML. The DataGrid's definition must include a style that sets a handler for previewing mouse clicks:

XML
<DataGrid ... >
  <DataGrid.CellStyle>
    <Style TargetType="DataGridCell">
      <EventSetter Event="PreviewMouseDown" Handler="DataGridCell_PreviewMouseDown"/>
    </Style>
  </DataGrid.CellStyle>
</DataGrid>

We will discuss the handler's code a bit later.

The key to this solution is a structure we use to keep notes about when a mouse click occurred and what the ScrollViewer's horizontal position was at that time. The structure can be a general-purpose one that stores state information with a timestamp. The one I chose to use is shown below:

C#
struct StateTimestamp<T> {

  //--------------------------------------
  #region Properties
  //--------------------------------------

  /// <summary>
  /// Any ephemeral datum we wish to store with a timestamp.
  /// </summary>
  /// <remarks>
  /// Be careful to avoid storing a type whose members could
  /// change unexpectedly before an instance of this structure
  /// serves its purpose!
  /// </remarks>

  public T SavedStateInfo { get; }

  public DateTime Timestamp { get; }

  #endregion Properties

  //--------------------------------------
  #region Public Methods
  //--------------------------------------

  public StateTimestamp( T savedStateInfo ) {
    SavedStateInfo = savedStateInfo;
    Timestamp = DateTime.Now;
  }

  public bool IsRecent( double withinMilliseconds ) =>
    ( ( DateTime.Now - Timestamp ).TotalMilliseconds <= withinMilliseconds );

  #endregion Public Methods

}

In the source code that accompanies this article, the file named DependencyObject_Extensions.cs offers two methods you may find generally useful in your WPF programming adventures. These help you at runtime to find references to objects created by the presentation framework, objects to which you are unable to assign names in your XAML code, but you know they will exist while the application is running.

In this case, we know the DataGrid will contain a ScrollViewer at runtime, and we need a reference to it in order to be able to attach our ScrollChanged event handler and to be able to read the value of HorizontalOffset at runtime.

We cannot search for that reference inside the window's constructor. The ScrollViewer will not exist until the grid actually appears onscreen. (If called in the window's constructor, MyGrid.FindVisualChild<ScrollViewer>() would return null.) Therefore, we must find another point in time after the grid has appeared to seek and save the reference to our ScrollViewer. In this sample app, the grid is onscreen by the time the window's ContentRendered event fires, so we can do what we need to do in our ContentRendered event handler.

Here is the relevant portion of the ContentRendered event handler. It refers to a member variable named m_gridScrollViewer, which retains the reference to the grid's ScrollViewer after we find it.

C#
if ( MyGrid.FindVisualChild<ScrollViewer>() is ScrollViewer sv ) {
  m_gridScrollViewer = sv;
  m_gridScrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
}

Now we are ready to discuss the two handlers which make the solution work. These handlers refer to a constant, AUTO_SCROLL_PREVENTION_MILLISECONDS which tells us for how long to suppress horizontal scrolls after a mouse is clicked in a grid cell; and a member variable, m_cellMouseClickInfo, which is a StateTimestamp<double> that stores the ScrollViewer's horizontal offset at the time the mouse was clicked.

C#
private void DataGridCell_PreviewMouseDown( object sender, MouseButtonEventArgs e ) {
  m_cellMouseClickInfo = new StateTimestamp<double>
                         ( savedStateInfo: m_gridScrollViewer.HorizontalOffset );
}

private void ScrollViewer_ScrollChanged( object sender, ScrollChangedEventArgs e ) {
  if ( m_cellMouseClickInfo.IsRecent
     ( withinMilliseconds: AUTO_SCROLL_PREVENTION_MILLISECONDS ) ) {
    // Remember: our saved state info is simply the grid's 
    // horizontal offset at the time the mouse was clicked
    m_gridScrollViewer.ScrollToHorizontalOffset( m_cellMouseClickInfo.SavedStateInfo );
  }
}

That's it! In my testing, clicking in any partially visible cell of a row caused the row to be selected without any flickering or jumping around whatsoever. I hope the solution works as well for you.

Why So Much Code?

If you examine the properties of the ScrollChangedEventArgs instance received by our ScrollChanged handler, you will soon wonder why my solution ignores the HorizontalChange property. Theoretically, one could merely note the time of every mouse click in a cell and make the ScrollChanged handler undo auto-scrolling by the amount of the horizontal change. I did try that, but found I could not rely upon the value of HorizontalChange to be the exact amount of adjustment needed. Horizontal scroll suppression often failed, and the grid would jump around a bit even when it seemed to be working.

The good news is, most of the sample code in the accompanying solution belongs to the DependencyObject_Extensions class and StateTimestamp structure, and these provide functionality you may well need elsewhere in your application. Less than 20 lines of code are specific to solving this problem in one grid in one window.

History

  • 24th August, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Creative Commons Attribution-ShareAlike 2.5 License


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAnother solution with RequestBringIntoView Pin
KirillOsenkov17-Jul-20 14:26
KirillOsenkov17-Jul-20 14:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.