A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

6236 lines
238 KiB

// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.VisualTree;
using Avalonia.Utilities;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using System.Linq;
using Avalonia.Input.Platform;
using System.ComponentModel.DataAnnotations;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Controls.Metadata;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Styling;
using Avalonia.Reactive;
namespace Avalonia.Controls
{
/// <summary>
/// Displays data in a customizable grid.
/// </summary>
[TemplatePart(DATAGRID_elementBottomRightCornerHeaderName, typeof(Visual))]
[TemplatePart(DATAGRID_elementColumnHeadersPresenterName, typeof(DataGridColumnHeadersPresenter))]
[TemplatePart(DATAGRID_elementFrozenColumnScrollBarSpacerName, typeof(Control))]
[TemplatePart(DATAGRID_elementHorizontalScrollbarName, typeof(ScrollBar))]
[TemplatePart(DATAGRID_elementRowsPresenterName, typeof(DataGridRowsPresenter))]
[TemplatePart(DATAGRID_elementTopLeftCornerHeaderName, typeof(ContentControl))]
[TemplatePart(DATAGRID_elementTopRightCornerHeaderName, typeof(ContentControl))]
[TemplatePart(DATAGRID_elementVerticalScrollbarName, typeof(ScrollBar))]
[PseudoClasses(":invalid", ":empty-rows", ":empty-columns")]
public partial class DataGrid : TemplatedControl
{
private const string DATAGRID_elementRowsPresenterName = "PART_RowsPresenter";
private const string DATAGRID_elementColumnHeadersPresenterName = "PART_ColumnHeadersPresenter";
private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "PART_FrozenColumnScrollBarSpacer";
private const string DATAGRID_elementHorizontalScrollbarName = "PART_HorizontalScrollbar";
private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader";
private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader";
private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner";
private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar";
internal const bool DATAGRID_defaultCanUserReorderColumns = true;
internal const bool DATAGRID_defaultCanUserResizeColumns = true;
internal const bool DATAGRID_defaultCanUserSortColumns = true;
/// <summary>
/// The default order to use for columns when there is no <see cref="DisplayAttribute.Order"/>
/// value available for the property.
/// </summary>
/// <remarks>
/// The value of 10,000 comes from the DataAnnotations spec, allowing
/// some properties to be ordered at the beginning and some at the end.
/// </remarks>
private const int DATAGRID_defaultColumnDisplayOrder = 10000;
private const double DATAGRID_horizontalGridLinesThickness = 1;
private const double DATAGRID_minimumRowHeaderWidth = 4;
private const double DATAGRID_minimumColumnHeaderHeight = 4;
internal const double DATAGRID_maximumStarColumnWidth = 10000;
internal const double DATAGRID_minimumStarColumnWidth = 0.001;
private const double DATAGRID_mouseWheelDelta = 50.0;
private const double DATAGRID_maxHeadersThickness = 32768;
private const double DATAGRID_defaultRowHeight = 22;
internal const double DATAGRID_defaultRowGroupSublevelIndent = 20;
private const double DATAGRID_defaultMinColumnWidth = 20;
private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity;
private List<Exception> _bindingValidationErrors;
private IDisposable _validationSubscription;
private INotifyCollectionChanged _topLevelGroup;
private ContentControl _clipboardContentControl;
private Visual _bottomRightCorner;
private DataGridColumnHeadersPresenter _columnHeadersPresenter;
private DataGridRowsPresenter _rowsPresenter;
private ScrollBar _vScrollBar;
private ScrollBar _hScrollBar;
private ContentControl _topLeftCornerHeader;
private ContentControl _topRightCornerHeader;
private Control _frozenColumnScrollBarSpacer;
// the sum of the widths in pixels of the scrolling columns preceding
// the first displayed scrolling column
private double _horizontalOffset;
// the number of pixels of the firstDisplayedScrollingCol which are not displayed
private double _negHorizontalOffset;
private byte _autoGeneratingColumnOperationCount;
private bool _areHandlersSuspended;
private bool _autoSizingColumns;
private IndexToValueTable<bool> _collapsedSlotsTable;
private Control _clickedElement;
// used to store the current column during a Reset
private int _desiredCurrentColumnIndex;
private int _editingColumnIndex;
// this is a workaround only for the scenarios where we need it, it is not all encompassing nor always updated
private RoutedEventArgs _editingEventArgs;
private bool _executingLostFocusActions;
private bool _flushCurrentCellChanged;
private bool _focusEditingControl;
private Visual _focusedObject;
private byte _horizontalScrollChangesIgnored;
private DataGridRow _focusedRow;
private bool _ignoreNextScrollBarsLayout;
// Nth row of rows 0..N that make up the RowHeightEstimate
private int _lastEstimatedRow;
private List<DataGridRow> _loadedRows;
// prevents reentry into the VerticalScroll event handler
private Queue<Action> _lostFocusActions;
private int _noSelectionChangeCount;
private int _noCurrentCellChangeCount;
private bool _makeFirstDisplayedCellCurrentCellPending;
private bool _measured;
private int? _mouseOverRowIndex; // -1 is used for the 'new row'
private DataGridColumn _previousCurrentColumn;
private object _previousCurrentItem;
private double[] _rowGroupHeightsByLevel;
private double _rowHeaderDesiredWidth;
private Size? _rowsPresenterAvailableSize;
private bool _scrollingByHeight;
private IndexToValueTable<bool> _showDetailsTable;
private bool _successfullyUpdatedSelection;
private DataGridSelectedItemsCollection _selectedItems;
private bool _temporarilyResetCurrentCell;
private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode.
// An approximation of the sum of the heights in pixels of the scrolling rows preceding
// the first displayed scrolling row. Since the scrolled off rows are discarded, the grid
// does not know their actual height. The heights used for the approximation are the ones
// set as the rows were scrolled off.
private double _verticalOffset;
private byte _verticalScrollChangesIgnored;
public event EventHandler<ScrollEventArgs> HorizontalScroll;
public event EventHandler<ScrollEventArgs> VerticalScroll;
/// <summary>
/// Identifies the CanUserReorderColumns dependency property.
/// </summary>
public static readonly StyledProperty<bool> CanUserReorderColumnsProperty =
AvaloniaProperty.Register<DataGrid, bool>(nameof(CanUserReorderColumns));
/// <summary>
/// Gets or sets a value that indicates whether the user can change
/// the column display order by dragging column headers with the mouse.
/// </summary>
public bool CanUserReorderColumns
{
get { return GetValue(CanUserReorderColumnsProperty); }
set { SetValue(CanUserReorderColumnsProperty, value); }
}
/// <summary>
/// Identifies the CanUserResizeColumns dependency property.
/// </summary>
public static readonly StyledProperty<bool> CanUserResizeColumnsProperty =
AvaloniaProperty.Register<DataGrid, bool>(nameof(CanUserResizeColumns));
/// <summary>
/// Gets or sets a value that indicates whether the user can adjust column widths using the mouse.
/// </summary>
public bool CanUserResizeColumns
{
get { return GetValue(CanUserResizeColumnsProperty); }
set { SetValue(CanUserResizeColumnsProperty, value); }
}
/// <summary>
/// Identifies the CanUserSortColumns dependency property.
/// </summary>
public static readonly StyledProperty<bool> CanUserSortColumnsProperty =
AvaloniaProperty.Register<DataGrid, bool>(nameof(CanUserSortColumns), true);
/// <summary>
/// Gets or sets a value that indicates whether the user can sort columns by clicking the column header.
/// </summary>
public bool CanUserSortColumns
{
get { return GetValue(CanUserSortColumnsProperty); }
set { SetValue(CanUserSortColumnsProperty, value); }
}
/// <summary>
/// Identifies the ColumnHeaderHeight dependency property.
/// </summary>
public static readonly StyledProperty<double> ColumnHeaderHeightProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(ColumnHeaderHeight),
defaultValue: double.NaN,
validate: IsValidColumnHeaderHeight);
private static bool IsValidColumnHeaderHeight(double value)
{
return double.IsNaN(value) ||
(value >= DATAGRID_minimumColumnHeaderHeight && value <= DATAGRID_maxHeadersThickness);
}
/// <summary>
/// Gets or sets the height of the column headers row.
/// </summary>
public double ColumnHeaderHeight
{
get { return GetValue(ColumnHeaderHeightProperty); }
set { SetValue(ColumnHeaderHeightProperty, value); }
}
/// <summary>
/// Identifies the ColumnWidth dependency property.
/// </summary>
public static readonly StyledProperty<DataGridLength> ColumnWidthProperty =
AvaloniaProperty.Register<DataGrid, DataGridLength>(nameof(ColumnWidth), defaultValue: DataGridLength.Auto);
/// <summary>
/// Identifies the <see cref="RowTheme"/> dependency property.
/// </summary>
public static readonly StyledProperty<ControlTheme> RowThemeProperty =
AvaloniaProperty.Register<DataGrid, ControlTheme>(nameof(RowTheme));
/// <summary>
/// Gets or sets the theme applied to all rows.
/// </summary>
public ControlTheme RowTheme
{
get { return GetValue(RowThemeProperty); }
set { SetValue(RowThemeProperty, value); }
}
/// <summary>
/// Identifies the <see cref="CellTheme"/> dependency property.
/// </summary>
public static readonly StyledProperty<ControlTheme> CellThemeProperty =
AvaloniaProperty.Register<DataGrid, ControlTheme>(nameof(CellTheme));
/// <summary>
/// Gets or sets the theme applied to all cells.
/// </summary>
public ControlTheme CellTheme
{
get { return GetValue(CellThemeProperty); }
set { SetValue(CellThemeProperty, value); }
}
/// <summary>
/// Identifies the <see cref="ColumnHeaderTheme"/> dependency property.
/// </summary>
public static readonly StyledProperty<ControlTheme> ColumnHeaderThemeProperty =
AvaloniaProperty.Register<DataGrid, ControlTheme>(nameof(ColumnHeaderTheme));
/// <summary>
/// Gets or sets the theme applied to all column headers.
/// </summary>
public ControlTheme ColumnHeaderTheme
{
get { return GetValue(ColumnHeaderThemeProperty); }
set { SetValue(ColumnHeaderThemeProperty, value); }
}
/// <summary>
/// Identifies the <see cref="RowGroupTheme"/> dependency property.
/// </summary>
public static readonly StyledProperty<ControlTheme> RowGroupThemeProperty =
AvaloniaProperty.Register<DataGrid, ControlTheme>(nameof(RowGroupTheme));
/// <summary>
/// Gets or sets the theme applied to all row groups.
/// </summary>
public ControlTheme RowGroupTheme
{
get { return GetValue(RowGroupThemeProperty); }
set { SetValue(RowGroupThemeProperty, value); }
}
/// <summary>
/// Gets or sets the standard width or automatic sizing mode of columns in the control.
/// </summary>
public DataGridLength ColumnWidth
{
get { return GetValue(ColumnWidthProperty); }
set { SetValue(ColumnWidthProperty, value); }
}
public static readonly StyledProperty<int> FrozenColumnCountProperty =
AvaloniaProperty.Register<DataGrid, int>(
nameof(FrozenColumnCount),
validate: ValidateFrozenColumnCount);
/// <summary>
/// Gets or sets the number of columns that the user cannot scroll horizontally.
/// </summary>
public int FrozenColumnCount
{
get { return GetValue(FrozenColumnCountProperty); }
set { SetValue(FrozenColumnCountProperty, value); }
}
private static bool ValidateFrozenColumnCount(int value) => value >= 0;
public static readonly StyledProperty<DataGridGridLinesVisibility> GridLinesVisibilityProperty =
AvaloniaProperty.Register<DataGrid, DataGridGridLinesVisibility>(nameof(GridLinesVisibility));
/// <summary>
/// Gets or sets a value that indicates which grid lines separating inner cells are shown.
/// </summary>
public DataGridGridLinesVisibility GridLinesVisibility
{
get { return GetValue(GridLinesVisibilityProperty); }
set { SetValue(GridLinesVisibilityProperty, value); }
}
public static readonly StyledProperty<DataGridHeadersVisibility> HeadersVisibilityProperty =
AvaloniaProperty.Register<DataGrid, DataGridHeadersVisibility>(nameof(HeadersVisibility));
/// <summary>
/// Gets or sets a value that indicates the visibility of row and column headers.
/// </summary>
public DataGridHeadersVisibility HeadersVisibility
{
get { return GetValue(HeadersVisibilityProperty); }
set { SetValue(HeadersVisibilityProperty, value); }
}
public static readonly StyledProperty<IBrush> HorizontalGridLinesBrushProperty =
AvaloniaProperty.Register<DataGrid, IBrush>(nameof(HorizontalGridLinesBrush));
/// <summary>
/// Gets or sets the <see cref="T:System.Windows.Media.Brush" /> that is used to paint grid lines separating rows.
/// </summary>
public IBrush HorizontalGridLinesBrush
{
get { return GetValue(HorizontalGridLinesBrushProperty); }
set { SetValue(HorizontalGridLinesBrushProperty, value); }
}
public static readonly StyledProperty<ScrollBarVisibility> HorizontalScrollBarVisibilityProperty =
AvaloniaProperty.Register<DataGrid, ScrollBarVisibility>(nameof(HorizontalScrollBarVisibility));
/// <summary>
/// Gets or sets a value that indicates how the horizontal scroll bar is displayed.
/// </summary>
public ScrollBarVisibility HorizontalScrollBarVisibility
{
get { return GetValue(HorizontalScrollBarVisibilityProperty); }
set { SetValue(HorizontalScrollBarVisibilityProperty, value); }
}
public static readonly StyledProperty<bool> IsReadOnlyProperty =
AvaloniaProperty.Register<DataGrid, bool>(nameof(IsReadOnly));
/// <summary>
/// Gets or sets a value that indicates whether the user can edit the values in the control.
/// </summary>
public bool IsReadOnly
{
get { return GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
public static readonly StyledProperty<bool> AreRowGroupHeadersFrozenProperty =
AvaloniaProperty.Register<DataGrid, bool>(
nameof(AreRowGroupHeadersFrozen),
defaultValue: true);
/// <summary>
/// Gets or sets a value that indicates whether the row group header sections
/// remain fixed at the width of the display area or can scroll horizontally.
/// </summary>
public bool AreRowGroupHeadersFrozen
{
get { return GetValue(AreRowGroupHeadersFrozenProperty); }
set { SetValue(AreRowGroupHeadersFrozenProperty, value); }
}
private void OnAreRowGroupHeadersFrozenChanged(AvaloniaPropertyChangedEventArgs e)
{
var value = (bool)e.NewValue;
ProcessFrozenColumnCount();
// Update elements in the RowGroupHeader that were previously frozen
if (value)
{
if (_rowsPresenter != null)
{
foreach (Control element in _rowsPresenter.Children)
{
if (element is DataGridRowGroupHeader groupHeader)
{
groupHeader.ClearFrozenStates();
}
}
}
}
}
/// <summary>
/// Defines the <see cref="IsScrollInertiaEnabled"/> property.
/// </summary>
public static readonly AttachedProperty<bool> IsScrollInertiaEnabledProperty =
ScrollViewer.IsScrollInertiaEnabledProperty.AddOwner<DataGrid>();
/// <summary>
/// Gets or sets whether scroll gestures should include inertia in their behavior and value.
/// </summary>
public bool IsScrollInertiaEnabled
{
get => GetValue(IsScrollInertiaEnabledProperty);
set => SetValue(IsScrollInertiaEnabledProperty, value);
}
private bool _isValid = true;
public static readonly DirectProperty<DataGrid, bool> IsValidProperty =
AvaloniaProperty.RegisterDirect<DataGrid, bool>(
nameof(IsValid),
o => o.IsValid);
public bool IsValid
{
get { return _isValid; }
internal set
{
SetAndRaise(IsValidProperty, ref _isValid, value);
PseudoClasses.Set(":invalid", !value);
}
}
public static readonly StyledProperty<double> MaxColumnWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(MaxColumnWidth),
defaultValue: DATAGRID_defaultMaxColumnWidth,
validate: IsValidColumnWidth);
private static bool IsValidColumnWidth(double value)
{
return !double.IsNaN(value) && value > 0;
}
/// <summary>
/// Gets or sets the maximum width of columns in the <see cref="T:Avalonia.Controls.DataGrid" /> .
/// </summary>
public double MaxColumnWidth
{
get { return GetValue(MaxColumnWidthProperty); }
set { SetValue(MaxColumnWidthProperty, value); }
}
public static readonly StyledProperty<double> MinColumnWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(MinColumnWidth),
defaultValue: DATAGRID_defaultMinColumnWidth,
validate: IsValidMinColumnWidth);
private static bool IsValidMinColumnWidth(double value)
{
return !double.IsNaN(value) && !double.IsPositiveInfinity(value) && value >= 0;
}
/// <summary>
/// Gets or sets the minimum width of columns in the <see cref="T:Avalonia.Controls.DataGrid" />.
/// </summary>
public double MinColumnWidth
{
get { return GetValue(MinColumnWidthProperty); }
set { SetValue(MinColumnWidthProperty, value); }
}
public static readonly StyledProperty<IBrush> RowBackgroundProperty =
AvaloniaProperty.Register<DataGrid, IBrush>(nameof(RowBackground));
/// <summary>
/// Gets or sets the <see cref="T:System.Windows.Media.Brush" /> that is used to paint row backgrounds.
/// </summary>
public IBrush RowBackground
{
get { return GetValue(RowBackgroundProperty); }
set { SetValue(RowBackgroundProperty, value); }
}
public static readonly StyledProperty<double> RowHeightProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(RowHeight),
defaultValue: double.NaN,
validate: IsValidRowHeight);
private static bool IsValidRowHeight(double value)
{
return double.IsNaN(value) ||
(value >= DataGridRow.DATAGRIDROW_minimumHeight &&
value <= DataGridRow.DATAGRIDROW_maximumHeight);
}
/// <summary>
/// Gets or sets the standard height of rows in the control.
/// </summary>
public double RowHeight
{
get { return GetValue(RowHeightProperty); }
set { SetValue(RowHeightProperty, value); }
}
public static readonly StyledProperty<double> RowHeaderWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(RowHeaderWidth),
defaultValue: double.NaN,
validate: IsValidRowHeaderWidth);
private static bool IsValidRowHeaderWidth(double value)
{
return double.IsNaN(value) ||
(value >= DATAGRID_minimumRowHeaderWidth &&
value <= DATAGRID_maxHeadersThickness);
}
/// <summary>
/// Gets or sets the width of the row header column.
/// </summary>
public double RowHeaderWidth
{
get { return GetValue(RowHeaderWidthProperty); }
set { SetValue(RowHeaderWidthProperty, value); }
}
public static readonly StyledProperty<DataGridSelectionMode> SelectionModeProperty =
AvaloniaProperty.Register<DataGrid, DataGridSelectionMode>(nameof(SelectionMode));
/// <summary>
/// Gets or sets the selection behavior of the data grid.
/// </summary>
public DataGridSelectionMode SelectionMode
{
get { return GetValue(SelectionModeProperty); }
set { SetValue(SelectionModeProperty, value); }
}
public static readonly StyledProperty<IBrush> VerticalGridLinesBrushProperty =
AvaloniaProperty.Register<DataGrid, IBrush>(nameof(VerticalGridLinesBrush));
/// <summary>
/// Gets or sets the <see cref="T:System.Windows.Media.Brush" /> that is used to paint grid lines separating columns.
/// </summary>
public IBrush VerticalGridLinesBrush
{
get { return GetValue(VerticalGridLinesBrushProperty); }
set { SetValue(VerticalGridLinesBrushProperty, value); }
}
public static readonly StyledProperty<ScrollBarVisibility> VerticalScrollBarVisibilityProperty =
AvaloniaProperty.Register<DataGrid, ScrollBarVisibility>(nameof(VerticalScrollBarVisibility));
/// <summary>
/// Gets or sets a value that indicates how the vertical scroll bar is displayed.
/// </summary>
public ScrollBarVisibility VerticalScrollBarVisibility
{
get { return GetValue(VerticalScrollBarVisibilityProperty); }
set { SetValue(VerticalScrollBarVisibilityProperty, value); }
}
public static readonly StyledProperty<ITemplate<Control>> DropLocationIndicatorTemplateProperty =
AvaloniaProperty.Register<DataGrid, ITemplate<Control>>(nameof(DropLocationIndicatorTemplate));
/// <summary>
/// Gets or sets the template that is used when rendering the column headers.
/// </summary>
public ITemplate<Control> DropLocationIndicatorTemplate
{
get { return GetValue(DropLocationIndicatorTemplateProperty); }
set { SetValue(DropLocationIndicatorTemplateProperty, value); }
}
private int _selectedIndex = -1;
private object _selectedItem;
public static readonly DirectProperty<DataGrid, int> SelectedIndexProperty =
AvaloniaProperty.RegisterDirect<DataGrid, int>(
nameof(SelectedIndex),
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets the index of the current selection.
/// </summary>
/// <returns>
/// The index of the current selection, or -1 if the selection is empty.
/// </returns>
public int SelectedIndex
{
get { return _selectedIndex; }
set { SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); }
}
public static readonly DirectProperty<DataGrid, object> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<DataGrid, object>(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets the data item corresponding to the selected row.
/// </summary>
public object SelectedItem
{
get { return _selectedItem; }
set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
}
public static readonly StyledProperty<DataGridClipboardCopyMode> ClipboardCopyModeProperty =
AvaloniaProperty.Register<DataGrid, DataGridClipboardCopyMode>(
nameof(ClipboardCopyMode),
defaultValue: DataGridClipboardCopyMode.ExcludeHeader);
/// <summary>
/// The property which determines how DataGrid content is copied to the Clipboard.
/// </summary>
public DataGridClipboardCopyMode ClipboardCopyMode
{
get { return GetValue(ClipboardCopyModeProperty); }
set { SetValue(ClipboardCopyModeProperty, value); }
}
public static readonly StyledProperty<bool> AutoGenerateColumnsProperty =
AvaloniaProperty.Register<DataGrid, bool>(nameof(AutoGenerateColumns));
/// <summary>
/// Gets or sets a value that indicates whether columns are created
/// automatically when the <see cref="P:Avalonia.Controls.DataGrid.ItemsSource" /> property is set.
/// </summary>
public bool AutoGenerateColumns
{
get { return GetValue(AutoGenerateColumnsProperty); }
set { SetValue(AutoGenerateColumnsProperty, value); }
}
private void OnAutoGenerateColumnsChanged(AvaloniaPropertyChangedEventArgs e)
{
var value = (bool)e.NewValue;
if (value)
{
InitializeElements(recycleRows: false);
}
else
{
RemoveAutoGeneratedColumns();
}
}
/// <summary>
/// Identifies the ItemsSource property.
/// </summary>
public static readonly StyledProperty<IEnumerable> ItemsSourceProperty =
AvaloniaProperty.Register<DataGrid, IEnumerable>(nameof(ItemsSource));
/// <summary>
/// Gets or sets a collection that is used to generate the content of the control.
/// </summary>
public IEnumerable ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly StyledProperty<bool> AreRowDetailsFrozenProperty =
AvaloniaProperty.Register<DataGrid, bool>(nameof(AreRowDetailsFrozen));
/// <summary>
/// Gets or sets a value that indicates whether the row details sections remain
/// fixed at the width of the display area or can scroll horizontally.
/// </summary>
public bool AreRowDetailsFrozen
{
get { return GetValue(AreRowDetailsFrozenProperty); }
set { SetValue(AreRowDetailsFrozenProperty, value); }
}
public static readonly StyledProperty<IDataTemplate> RowDetailsTemplateProperty =
AvaloniaProperty.Register<DataGrid, IDataTemplate>(nameof(RowDetailsTemplate));
/// <summary>
/// Gets or sets the template that is used to display the content of the details section of rows.
/// </summary>
public IDataTemplate RowDetailsTemplate
{
get { return GetValue(RowDetailsTemplateProperty); }
set { SetValue(RowDetailsTemplateProperty, value); }
}
public static readonly StyledProperty<DataGridRowDetailsVisibilityMode> RowDetailsVisibilityModeProperty =
AvaloniaProperty.Register<DataGrid, DataGridRowDetailsVisibilityMode>(nameof(RowDetailsVisibilityMode));
/// <summary>
/// Gets or sets a value that indicates when the details sections of rows are displayed.
/// </summary>
public DataGridRowDetailsVisibilityMode RowDetailsVisibilityMode
{
get { return GetValue(RowDetailsVisibilityModeProperty); }
set { SetValue(RowDetailsVisibilityModeProperty, value); }
}
public static readonly DirectProperty<DataGrid, IDataGridCollectionView> CollectionViewProperty =
AvaloniaProperty.RegisterDirect<DataGrid, IDataGridCollectionView>(nameof(CollectionView),
o => o.CollectionView);
/// <summary>
/// Gets current <see cref="IDataGridCollectionView"/>.
/// </summary>
public IDataGridCollectionView CollectionView =>
DataConnection.CollectionView;
static DataGrid()
{
AffectsMeasure<DataGrid>(
ColumnHeaderHeightProperty,
HorizontalScrollBarVisibilityProperty,
VerticalScrollBarVisibilityProperty);
ItemsSourceProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnItemsSourcePropertyChanged(e));
CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnCanUserResizeColumnsChanged(e));
ColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnColumnWidthChanged(e));
FrozenColumnCountProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnFrozenColumnCountChanged(e));
GridLinesVisibilityProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnGridLinesVisibilityChanged(e));
HeadersVisibilityProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnHeadersVisibilityChanged(e));
HorizontalGridLinesBrushProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnHorizontalGridLinesBrushChanged(e));
IsReadOnlyProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnIsReadOnlyChanged(e));
MaxColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnMaxColumnWidthChanged(e));
MinColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnMinColumnWidthChanged(e));
RowHeightProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowHeightChanged(e));
RowHeaderWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowHeaderWidthChanged(e));
SelectionModeProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnSelectionModeChanged(e));
VerticalGridLinesBrushProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnVerticalGridLinesBrushChanged(e));
SelectedIndexProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnSelectedIndexChanged(e));
SelectedItemProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnSelectedItemChanged(e));
IsEnabledProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.DataGrid_IsEnabledChanged(e));
AreRowGroupHeadersFrozenProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnAreRowGroupHeadersFrozenChanged(e));
RowDetailsTemplateProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsTemplateChanged(e));
RowDetailsVisibilityModeProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsVisibilityModeChanged(e));
AutoGenerateColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnAutoGenerateColumnsChanged(e));
FocusableProperty.OverrideDefaultValue<DataGrid>(true);
}
/// <summary>
/// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGrid" /> class.
/// </summary>
public DataGrid()
{
KeyDown += DataGrid_KeyDown;
KeyUp += DataGrid_KeyUp;
//TODO: Check if override works
GotFocus += DataGrid_GotFocus;
LostFocus += DataGrid_LostFocus;
_loadedRows = new List<DataGridRow>();
_lostFocusActions = new Queue<Action>();
_selectedItems = new DataGridSelectedItemsCollection(this);
RowGroupHeadersTable = new IndexToValueTable<DataGridRowGroupInfo>();
_bindingValidationErrors = new List<Exception>();
DisplayData = new DataGridDisplayData(this);
ColumnsInternal = CreateColumnsInstance();
ColumnsInternal.CollectionChanged += ColumnsInternal_CollectionChanged;
RowHeightEstimate = DATAGRID_defaultRowHeight;
RowDetailsHeightEstimate = 0;
_rowHeaderDesiredWidth = 0;
DataConnection = new DataGridDataConnection(this);
_showDetailsTable = new IndexToValueTable<bool>();
_collapsedSlotsTable = new IndexToValueTable<bool>();
AnchorSlot = -1;
_lastEstimatedRow = -1;
_editingColumnIndex = -1;
_mouseOverRowIndex = null;
CurrentCellCoordinates = new DataGridCellCoordinates(-1, -1);
RowGroupHeaderHeightEstimate = DATAGRID_defaultRowHeight;
UpdatePseudoClasses();
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new DataGridAutomationPeer(this);
}
private void SetValueNoCallback<T>(AvaloniaProperty<T> property, T value, BindingPriority priority = BindingPriority.LocalValue)
{
_areHandlersSuspended = true;
try
{
SetValue(property, value, priority);
}
finally
{
_areHandlersSuspended = false;
}
}
private void OnRowDetailsVisibilityModeChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateRowDetailsVisibilityMode((DataGridRowDetailsVisibilityMode)e.NewValue);
}
private void OnRowDetailsTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{
// Update the RowDetails templates if necessary
if (_rowsPresenter != null)
{
foreach (DataGridRow row in GetAllRows())
{
if (GetRowDetailsVisibility(row.Index))
{
// DetailsPreferredHeight is initialized when the DetailsElement's size changes.
row.ApplyDetailsTemplate(initializeDetailsPreferredHeight: false);
}
}
}
UpdateRowDetailsHeightEstimate();
InvalidateMeasure();
}
/// <summary>
/// ItemsSourceProperty property changed handler.
/// </summary>
/// <param name="e">The event arguments.</param>
private void OnItemsSourcePropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
Debug.Assert(DataConnection != null);
var oldCollectionView = DataConnection.CollectionView;
var oldValue = (IEnumerable)e.OldValue;
var newItemsSource = (IEnumerable)e.NewValue;
if (LoadingOrUnloadingRow)
{
SetValueNoCallback(ItemsSourceProperty, oldValue);
throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows();
}
// Try to commit edit on the old DataSource, but force a cancel if it fails
if (!CommitEdit())
{
CancelEdit(DataGridEditingUnit.Row, false);
}
DataConnection.UnWireEvents(DataConnection.DataSource);
DataConnection.ClearDataProperties();
ClearRowGroupHeadersTable();
// The old selected indexes are no longer relevant. There's a perf benefit from
// updating the selected indexes with a null DataSource, because we know that all
// of the previously selected indexes have been removed from selection
DataConnection.DataSource = null;
_selectedItems.UpdateIndexes();
CoerceSelectedItem();
// Wrap an IEnumerable in an ICollectionView if it's not already one
bool setDefaultSelection = false;
if (newItemsSource is IDataGridCollectionView newCollectionView)
{
setDefaultSelection = true;
}
else
{
newCollectionView = newItemsSource is not null
? DataGridDataConnection.CreateView(newItemsSource)
: default;
}
DataConnection.DataSource = newCollectionView;
if (oldCollectionView != DataConnection.CollectionView)
{
RaisePropertyChanged(CollectionViewProperty,
oldCollectionView,
newCollectionView);
}
if (DataConnection.DataSource != null)
{
// Setup the column headers
if (DataConnection.DataType != null)
{
foreach (var column in ColumnsInternal.GetDisplayedColumns())
{
if (column is DataGridBoundColumn boundColumn)
{
boundColumn.SetHeaderFromBinding();
}
}
}
DataConnection.WireEvents(DataConnection.DataSource);
}
// Wait for the current cell to be set before we raise any SelectionChanged events
_makeFirstDisplayedCellCurrentCellPending = true;
// Clear out the old rows and remove the generated columns
ClearRows(false); //recycle
RemoveAutoGeneratedColumns();
// Set the SlotCount (from the data count and number of row group headers) before we make the default selection
PopulateRowGroupHeadersTable();
SelectedItem = null;
if (DataConnection.CollectionView != null && setDefaultSelection)
{
SelectedItem = DataConnection.CollectionView.CurrentItem;
}
// Treat this like the DataGrid has never been measured because all calculations at
// this point are invalid until the next layout cycle. For instance, the ItemsSource
// can be set when the DataGrid is not part of the visual tree
_measured = false;
InvalidateMeasure();
UpdatePseudoClasses();
}
}
private void ColumnsInternal_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add
|| e.Action == NotifyCollectionChangedAction.Remove
|| e.Action == NotifyCollectionChangedAction.Reset)
{
UpdatePseudoClasses();
}
}
internal void UpdatePseudoClasses()
{
PseudoClasses.Set(":empty-columns", !ColumnsInternal.GetVisibleColumns().Any());
PseudoClasses.Set(":empty-rows", !DataConnection.Any());
}
private void OnSelectedIndexChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
int index = (int)e.NewValue;
// GetDataItem returns null if index is >= Count, we do not check newValue
// against Count here to avoid enumerating through an Enumerable twice
// Setting SelectedItem coerces the finally value of the SelectedIndex
object newSelectedItem = (index < 0) ? null : DataConnection.GetDataItem(index);
SelectedItem = newSelectedItem;
if (SelectedItem != newSelectedItem)
{
SetValueNoCallback(SelectedIndexProperty, (int)e.OldValue);
}
}
}
private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
int rowIndex = (e.NewValue == null) ? -1 : DataConnection.IndexOf(e.NewValue);
if (rowIndex == -1)
{
// If the Item is null or it's not found, clear the Selection
if (!CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
{
// Edited value couldn't be committed or aborted
SetValueNoCallback(SelectedItemProperty, e.OldValue);
return;
}
// Clear all row selections
ClearRowSelection(resetAnchorSlot: true);
if (DataConnection.CollectionView != null)
{
DataConnection.CollectionView.MoveCurrentTo(null);
}
}
else
{
int slot = SlotFromRowIndex(rowIndex);
if (slot != CurrentSlot)
{
if (!CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
{
// Edited value couldn't be committed or aborted
SetValueNoCallback(SelectedItemProperty, e.OldValue);
return;
}
if (slot >= SlotCount || slot < -1)
{
if (DataConnection.CollectionView != null)
{
DataConnection.CollectionView.MoveCurrentToPosition(rowIndex);
}
}
}
int oldSelectedIndex = SelectedIndex;
SetValueNoCallback(SelectedIndexProperty, rowIndex);
try
{
_noSelectionChangeCount++;
int columnIndex = CurrentColumnIndex;
if (columnIndex == -1)
{
columnIndex = FirstDisplayedNonFillerColumnIndex;
}
if (IsSlotOutOfSelectionBounds(slot))
{
ClearRowSelection(slotException: slot, setAnchorSlot: true);
return;
}
UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
}
finally
{
NoSelectionChangeCount--;
}
if (!_successfullyUpdatedSelection)
{
SetValueNoCallback(SelectedIndexProperty, oldSelectedIndex);
SetValueNoCallback(SelectedItemProperty, e.OldValue);
}
}
}
}
private void OnVerticalGridLinesBrushChanged(AvaloniaPropertyChangedEventArgs e)
{
if (_rowsPresenter != null)
{
foreach (DataGridRow row in GetAllRows())
{
row.EnsureGridLines();
}
}
}
private void OnSelectionModeChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
ClearRowSelection(resetAnchorSlot: true);
}
}
private void OnRowHeaderWidthChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
EnsureRowHeaderWidth();
}
}
private void OnRowHeightChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
InvalidateRowHeightEstimate();
// Re-measure all the rows due to the Height change
InvalidateRowsMeasure(invalidateIndividualElements: true);
// DataGrid needs to update the layout information and the ScrollBars
InvalidateMeasure();
}
}
private void OnMinColumnWidthChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
double oldValue = (double)e.OldValue;
foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns())
{
OnColumnMinWidthChanged(column, Math.Max(column.MinWidth, oldValue));
}
}
}
private void OnMaxColumnWidthChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
var oldValue = (double)e.OldValue;
foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns())
{
OnColumnMaxWidthChanged(column, Math.Min(column.MaxWidth, oldValue));
}
}
}
private void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended)
{
var value = (bool)e.NewValue;
if (value && !CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
{
CancelEdit(DataGridEditingUnit.Row, raiseEvents: false);
}
}
}
private void OnHorizontalGridLinesBrushChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_areHandlersSuspended && _rowsPresenter != null)
{
foreach (DataGridRow row in GetAllRows())
{
row.EnsureGridLines();
}
}
}
private void OnHeadersVisibilityChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldValue = (DataGridHeadersVisibility)e.OldValue;
var newValue = (DataGridHeadersVisibility)e.NewValue;
bool hasFlags(DataGridHeadersVisibility value, DataGridHeadersVisibility flags) => ((value & flags) == flags);
bool newValueCols = hasFlags(newValue, DataGridHeadersVisibility.Column);
bool newValueRows = hasFlags(newValue, DataGridHeadersVisibility.Row);
bool oldValueCols = hasFlags(oldValue, DataGridHeadersVisibility.Column);
bool oldValueRows = hasFlags(oldValue, DataGridHeadersVisibility.Row);
// Columns
if (newValueCols != oldValueCols)
{
if (_columnHeadersPresenter != null)
{
EnsureColumnHeadersVisibility();
if (!newValueCols)
{
_columnHeadersPresenter.Measure(default);
}
else
{
EnsureVerticalGridLines();
}
InvalidateMeasure();
}
}
// Rows
if (newValueRows != oldValueRows)
{
if (_rowsPresenter != null)
{
foreach (Control element in _rowsPresenter.Children)
{
if (element is DataGridRow row)
{
row.EnsureHeaderStyleAndVisibility(null);
if (newValueRows)
{
row.ApplyState();
row.EnsureHeaderVisibility();
}
}
else if (element is DataGridRowGroupHeader rowGroupHeader)
{
rowGroupHeader.EnsureHeaderVisibility();
}
}
InvalidateRowHeightEstimate();
InvalidateRowsMeasure(invalidateIndividualElements: true);
}
}
if (_topLeftCornerHeader != null)
{
_topLeftCornerHeader.IsVisible = newValueRows && newValueCols;
if (_topLeftCornerHeader.IsVisible)
{
_topLeftCornerHeader.Measure(default);
}
}
}
private void OnGridLinesVisibilityChanged(AvaloniaPropertyChangedEventArgs e)
{
foreach (DataGridRow row in GetAllRows())
{
row.EnsureGridLines();
row.InvalidateHorizontalArrange();
}
}
private void OnFrozenColumnCountChanged(AvaloniaPropertyChangedEventArgs e)
{
ProcessFrozenColumnCount();
}
private void ProcessFrozenColumnCount()
{
CorrectColumnFrozenStates();
ComputeScrollBarsLayout();
InvalidateColumnHeadersArrange();
InvalidateCellsArrange();
}
private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e)
{
var value = (DataGridLength)e.NewValue;
foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns())
{
if (column.InheritsWidth)
{
column.SetWidthInternalNoCallback(value);
}
}
EnsureHorizontalLayout();
}
private void OnCanUserResizeColumnsChanged(AvaloniaPropertyChangedEventArgs e)
{
EnsureHorizontalLayout();
}
/// <summary>
/// Occurs one time for each public, non-static property in the bound data type when the
/// <see cref="P:Avalonia.Controls.DataGrid.ItemsSource" /> property is changed and the
/// <see cref="P:Avalonia.Controls.DataGrid.AutoGenerateColumns" /> property is true.
/// </summary>
public event EventHandler<DataGridAutoGeneratingColumnEventArgs> AutoGeneratingColumn;
/// <summary>
/// Occurs before a cell or row enters editing mode.
/// </summary>
public event EventHandler<DataGridBeginningEditEventArgs> BeginningEdit;
/// <summary>
/// Occurs after cell editing has ended.
/// </summary>
public event EventHandler<DataGridCellEditEndedEventArgs> CellEditEnded;
/// <summary>
/// Occurs immediately before cell editing has ended.
/// </summary>
public event EventHandler<DataGridCellEditEndingEventArgs> CellEditEnding;
/// <summary>
/// Occurs when cell is mouse-pressed.
/// </summary>
public event EventHandler<DataGridCellPointerPressedEventArgs> CellPointerPressed;
/// <summary>
/// Occurs when the <see cref="P:Avalonia.Controls.DataGridColumn.DisplayIndex" />
/// property of a column changes.
/// </summary>
public event EventHandler<DataGridColumnEventArgs> ColumnDisplayIndexChanged;
/// <summary>
/// Raised when column reordering ends, to allow subscribers to clean up.
/// </summary>
public event EventHandler<DataGridColumnEventArgs> ColumnReordered;
/// <summary>
/// Raised when starting a column reordering action. Subscribers to this event can
/// set tooltip and caret UIElements, constrain tooltip position, indicate that
/// a preview should be shown, or cancel reordering.
/// </summary>
public event EventHandler<DataGridColumnReorderingEventArgs> ColumnReordering;
/// <summary>
/// Occurs when a different cell becomes the current cell.
/// </summary>
public event EventHandler<EventArgs> CurrentCellChanged;
/// <summary>
/// Occurs after a <see cref="T:Avalonia.Controls.DataGridRow" />
/// is instantiated, so that you can customize it before it is used.
/// </summary>
public event EventHandler<DataGridRowEventArgs> LoadingRow;
/// <summary>
/// Occurs when a cell in a <see cref="T:Avalonia.Controls.DataGridTemplateColumn" /> enters editing mode.
///
/// </summary>
public event EventHandler<DataGridPreparingCellForEditEventArgs> PreparingCellForEdit;
/// <summary>
/// Occurs when the row has been successfully committed or cancelled.
/// </summary>
public event EventHandler<DataGridRowEditEndedEventArgs> RowEditEnded;
/// <summary>
/// Occurs immediately before the row has been successfully committed or cancelled.
/// </summary>
public event EventHandler<DataGridRowEditEndingEventArgs> RowEditEnding;
public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
RoutedEvent.Register<DataGrid, SelectionChangedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Bubble);
/// <summary>
/// Occurs when the <see cref="P:Avalonia.Controls.DataGrid.SelectedItem" /> or
/// <see cref="P:Avalonia.Controls.DataGrid.SelectedItems" /> property value changes.
/// </summary>
public event EventHandler<SelectionChangedEventArgs> SelectionChanged
{
add { AddHandler(SelectionChangedEvent, value); }
remove { RemoveHandler(SelectionChangedEvent, value); }
}
/// <summary>
/// Occurs when the <see cref="DataGridColumn"/> sorting request is triggered.
/// </summary>
public event EventHandler<DataGridColumnEventArgs> Sorting;
/// <summary>
/// Occurs when a <see cref="T:Avalonia.Controls.DataGridRow" />
/// object becomes available for reuse.
/// </summary>
public event EventHandler<DataGridRowEventArgs> UnloadingRow;
/// <summary>
/// Occurs when a new row details template is applied to a row, so that you can customize
/// the details section before it is used.
/// </summary>
public event EventHandler<DataGridRowDetailsEventArgs> LoadingRowDetails;
/// <summary>
/// Occurs when the <see cref="P:Avalonia.Controls.DataGrid.RowDetailsVisibilityMode" />
/// property value changes.
/// </summary>
public event EventHandler<DataGridRowDetailsEventArgs> RowDetailsVisibilityChanged;
/// <summary>
/// Occurs when a row details element becomes available for reuse.
/// </summary>
public event EventHandler<DataGridRowDetailsEventArgs> UnloadingRowDetails;
/// <summary>
/// Gets a collection that contains all the columns in the control.
/// </summary>
public ObservableCollection<DataGridColumn> Columns
{
get
{
// we use a backing field here because the field's type
// is a subclass of the property's
return ColumnsInternal;
}
}
/// <summary>
/// Gets or sets the column that contains the current cell.
/// </summary>
public DataGridColumn CurrentColumn
{
get
{
if (CurrentColumnIndex == -1)
{
return null;
}
Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
return ColumnsItemsInternal[CurrentColumnIndex];
}
set
{
DataGridColumn dataGridColumn = value;
if (dataGridColumn == null)
{
throw DataGridError.DataGrid.ValueCannotBeSetToNull("value", "CurrentColumn");
}
if (CurrentColumn != dataGridColumn)
{
if (dataGridColumn.OwningGrid != this)
{
// Provided column does not belong to this DataGrid
throw DataGridError.DataGrid.ColumnNotInThisDataGrid();
}
if (!dataGridColumn.IsVisible)
{
// CurrentColumn cannot be set to an invisible column
throw DataGridError.DataGrid.ColumnCannotBeCollapsed();
}
if (CurrentSlot == -1)
{
// There is no current row so the current column cannot be set
throw DataGridError.DataGrid.NoCurrentRow();
}
bool beginEdit = _editingColumnIndex != -1;
//exitEditingMode, keepFocus, raiseEvents
if (!EndCellEdit(DataGridEditAction.Commit, true, ContainsFocus, true))
{
// Edited value couldn't be committed or aborted
return;
}
UpdateSelectionAndCurrency(dataGridColumn.Index, CurrentSlot, DataGridSelectionAction.None, false); //scrollIntoView
Debug.Assert(_successfullyUpdatedSelection);
if (beginEdit &&
_editingColumnIndex == -1 &&
CurrentSlot != -1 &&
CurrentColumnIndex != -1 &&
CurrentColumnIndex == dataGridColumn.Index &&
dataGridColumn.OwningGrid == this &&
!GetColumnEffectiveReadOnlyState(dataGridColumn))
{
// Returning to editing mode since the grid was in that mode prior to the EndCellEdit call above.
BeginCellEdit(new RoutedEventArgs());
}
}
}
}
/// <summary>
/// Gets a list that contains the data items corresponding to the selected rows.
/// </summary>
public IList SelectedItems
{
get { return _selectedItems as IList; }
}
internal DataGridColumnCollection ColumnsInternal
{
get;
}
internal int AnchorSlot
{
get;
private set;
}
internal double ActualRowHeaderWidth
{
get
{
if (!AreRowHeadersVisible)
{
return 0;
}
else
{
return !double.IsNaN(RowHeaderWidth) ? RowHeaderWidth : RowHeadersDesiredWidth;
}
}
}
internal double ActualRowsPresenterHeight
{
get
{
if (_rowsPresenter != null)
{
return _rowsPresenter.Bounds.Height;
}
return 0;
}
}
internal bool AreColumnHeadersVisible
{
get
{
return (HeadersVisibility & DataGridHeadersVisibility.Column) == DataGridHeadersVisibility.Column;
}
}
internal bool AreRowHeadersVisible
{
get
{
return (HeadersVisibility & DataGridHeadersVisibility.Row) == DataGridHeadersVisibility.Row;
}
}
/// <summary>
/// Indicates whether or not at least one auto-sizing column is waiting for all the rows
/// to be measured before its final width is determined.
/// </summary>
internal bool AutoSizingColumns
{
get
{
return _autoSizingColumns;
}
set
{
if (_autoSizingColumns && !value && ColumnsInternal != null)
{
double adjustment = CellsWidth - ColumnsInternal.VisibleEdgedColumnsWidth;
AdjustColumnWidths(0, adjustment, false);
foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns())
{
column.IsInitialDesiredWidthDetermined = true;
}
ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
ComputeScrollBarsLayout();
InvalidateColumnHeadersMeasure();
InvalidateRowsMeasure(true);
}
_autoSizingColumns = value;
}
}
internal double AvailableSlotElementRoom
{
get;
set;
}
internal double CellsEstimatedHeight
{
get
{
return RowsPresenterAvailableSize?.Height ?? 0;
}
}
// Width currently available for cells this value is smaller. This width is reduced by the existence of RowHeaders
// or a vertical scrollbar. Layout is asynchronous so changes to the RowHeaders or the vertical scrollbar are
// not reflected immediately
internal double CellsWidth
{
get
{
double rowsWidth = double.PositiveInfinity;
if (RowsPresenterAvailableSize.HasValue)
{
rowsWidth = Math.Max(0, RowsPresenterAvailableSize.Value.Width - ActualRowHeaderWidth);
}
return double.IsPositiveInfinity(rowsWidth) ? ColumnsInternal.VisibleEdgedColumnsWidth : rowsWidth;
}
}
internal DataGridColumnHeadersPresenter ColumnHeaders => _columnHeadersPresenter;
internal List<DataGridColumn> ColumnsItemsInternal => ColumnsInternal.ItemsInternal;
internal bool ContainsFocus
{
get;
private set;
}
internal int CurrentColumnIndex
{
get
{
return CurrentCellCoordinates.ColumnIndex;
}
private set
{
CurrentCellCoordinates.ColumnIndex = value;
}
}
internal int CurrentSlot
{
get
{
return CurrentCellCoordinates.Slot;
}
private set
{
CurrentCellCoordinates.Slot = value;
}
}
internal DataGridDataConnection DataConnection
{
get;
private set;
}
internal DataGridDisplayData DisplayData
{
get;
private set;
}
internal int EditingColumnIndex
{
get;
private set;
}
internal DataGridRow EditingRow
{
get;
private set;
}
internal double FirstDisplayedScrollingColumnHiddenWidth => _negHorizontalOffset;
// When the RowsPresenter's width increases, the HorizontalOffset will be incorrect until
// the scrollbar's layout is recalculated, which doesn't occur until after the cells are measured.
// This property exists to account for this scenario, and avoid collapsing the incorrect cells.
internal double HorizontalAdjustment
{
get;
private set;
}
internal static double HorizontalGridLinesThickness => DATAGRID_horizontalGridLinesThickness;
// the sum of the widths in pixels of the scrolling columns preceding
// the first displayed scrolling column
internal double HorizontalOffset
{
get
{
return _horizontalOffset;
}
set
{
if (value < 0)
{
value = 0;
}
double widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth);
if (value > widthNotVisible)
{
value = widthNotVisible;
}
if (value == _horizontalOffset)
{
return;
}
if (_hScrollBar != null && value != _hScrollBar.Value)
{
_hScrollBar.Value = value;
}
_horizontalOffset = value;
DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
// update the lastTotallyDisplayedScrollingCol
ComputeDisplayedColumns();
}
}
internal ScrollBar HorizontalScrollBar => _hScrollBar;
internal IndexToValueTable<DataGridRowGroupInfo> RowGroupHeadersTable
{
get;
private set;
}
internal bool LoadingOrUnloadingRow
{
get;
private set;
}
internal bool InDisplayIndexAdjustments
{
get;
set;
}
internal int? MouseOverRowIndex
{
get
{
return _mouseOverRowIndex;
}
set
{
if (_mouseOverRowIndex != value)
{
DataGridRow oldMouseOverRow = null;
if (_mouseOverRowIndex.HasValue)
{
int oldSlot = SlotFromRowIndex(_mouseOverRowIndex.Value);
if (IsSlotVisible(oldSlot))
{
oldMouseOverRow = DisplayData.GetDisplayedElement(oldSlot) as DataGridRow;
}
}
_mouseOverRowIndex = value;
// State for the old row needs to be applied after setting the new value
if (oldMouseOverRow != null)
{
oldMouseOverRow.ApplyState();
}
if (_mouseOverRowIndex.HasValue)
{
int newSlot = SlotFromRowIndex(_mouseOverRowIndex.Value);
if (IsSlotVisible(newSlot))
{
DataGridRow newMouseOverRow = DisplayData.GetDisplayedElement(newSlot) as DataGridRow;
Debug.Assert(newMouseOverRow != null);
if (newMouseOverRow != null)
{
newMouseOverRow.ApplyState();
}
}
}
}
}
}
internal double NegVerticalOffset
{
get;
private set;
}
internal int NoCurrentCellChangeCount
{
get
{
return _noCurrentCellChangeCount;
}
set
{
_noCurrentCellChangeCount = value;
if (value == 0)
{
FlushCurrentCellChanged();
}
}
}
internal double RowDetailsHeightEstimate
{
get;
private set;
}
internal double RowHeadersDesiredWidth
{
get
{
return _rowHeaderDesiredWidth;
}
set
{
// We only auto grow
if (_rowHeaderDesiredWidth < value)
{
double oldActualRowHeaderWidth = ActualRowHeaderWidth;
_rowHeaderDesiredWidth = value;
if (oldActualRowHeaderWidth != ActualRowHeaderWidth)
{
EnsureRowHeaderWidth();
}
}
}
}
internal double RowGroupHeaderHeightEstimate
{
get;
private set;
}
internal double RowHeightEstimate
{
get;
private set;
}
internal Size? RowsPresenterAvailableSize
{
get
{
return _rowsPresenterAvailableSize;
}
set
{
if (_rowsPresenterAvailableSize.HasValue && value.HasValue && value.Value.Width > RowsPresenterAvailableSize.Value.Width)
{
// When the available cells width increases, the horizontal offset can be incorrect.
// Store away an adjustment to use during the CellsPresenter's measure, so that the
// ShouldDisplayCell method correctly determines if a cell will be in view.
//
// | h. offset | new available cells width |
// |-------------->|----------------------------------------->|
// __________________________________________________ |
// | | | | | |
// | column0 | column1 | column2 | column3 |<----->|
// | | | | | adj. |
//
double adjustment = (_horizontalOffset + value.Value.Width) - ColumnsInternal.VisibleEdgedColumnsWidth;
HorizontalAdjustment = Math.Min(HorizontalOffset, Math.Max(0, adjustment));
}
else
{
HorizontalAdjustment = 0;
}
_rowsPresenterAvailableSize = value;
}
}
internal double[] RowGroupSublevelIndents
{
get;
private set;
}
// This flag indicates whether selection has actually changed during a selection operation,
// and exists to ensure that FlushSelectionChanged doesn't unnecessarily raise SelectionChanged.
internal bool SelectionHasChanged
{
get;
set;
}
internal int SlotCount
{
get;
private set;
}
/// <summary>
/// Indicates whether or not to use star-sizing logic. If the DataGrid has infinite available space,
/// then star sizing doesn't make sense. In this case, all star columns grow to a predefined size of
/// 10,000 pixels in order to show the developer that star columns shouldn't be used.
/// </summary>
internal bool UsesStarSizing
{
get
{
if (ColumnsInternal != null)
{
return ColumnsInternal.VisibleStarColumnCount > 0 &&
(!RowsPresenterAvailableSize.HasValue || !double.IsPositiveInfinity(RowsPresenterAvailableSize.Value.Width));
}
return false;
}
}
internal ScrollBar VerticalScrollBar => _vScrollBar;
internal int VisibleSlotCount
{
get;
set;
}
/// <summary>
/// Gets the data item bound to the row that contains the current cell.
/// </summary>
protected object CurrentItem
{
get
{
if (CurrentSlot == -1 || ItemsSource == null || RowGroupHeadersTable.Contains(CurrentSlot))
{
return null;
}
return DataConnection.GetDataItem(RowIndexFromSlot(CurrentSlot));
}
}
private DataGridCellCoordinates CurrentCellCoordinates
{
get;
set;
}
private int FirstDisplayedNonFillerColumnIndex
{
get
{
DataGridColumn column = ColumnsInternal.FirstVisibleNonFillerColumn;
if (column != null)
{
if (column.IsFrozen)
{
return column.Index;
}
else
{
if (DisplayData.FirstDisplayedScrollingCol >= column.Index)
{
return DisplayData.FirstDisplayedScrollingCol;
}
else
{
return column.Index;
}
}
}
return -1;
}
}
private bool IsHorizontalScrollBarOverCells
{
get
{
return _columnHeadersPresenter != null && Grid.GetColumnSpan(_columnHeadersPresenter) == 2;
}
}
private bool IsVerticalScrollBarOverCells
{
get
{
return _rowsPresenter != null && Grid.GetRowSpan(_rowsPresenter) == 2;
}
}
private int NoSelectionChangeCount
{
get
{
return _noSelectionChangeCount;
}
set
{
_noSelectionChangeCount = value;
if (value == 0)
{
FlushSelectionChanged();
}
}
}
/// <summary>
/// Enters editing mode for the current cell and current row (if they're not already in editing mode).
/// </summary>
/// <returns>True if operation was successful. False otherwise.</returns>
public bool BeginEdit()
{
return BeginEdit(null);
}
/// <summary>
/// Enters editing mode for the current cell and current row (if they're not already in editing mode).
/// </summary>
/// <param name="editingEventArgs">Provides information about the user gesture that caused the call to BeginEdit. Can be null.</param>
/// <returns>True if operation was successful. False otherwise.</returns>
public bool BeginEdit(RoutedEventArgs editingEventArgs)
{
if (CurrentColumnIndex == -1 || !GetRowSelection(CurrentSlot))
{
return false;
}
Debug.Assert(CurrentColumnIndex >= 0);
Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(CurrentSlot >= -1);
Debug.Assert(CurrentSlot < SlotCount);
Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot);
if (GetColumnEffectiveReadOnlyState(CurrentColumn))
{
// Current column is read-only
return false;
}
return BeginCellEdit(editingEventArgs);
}
/// <summary>
/// Cancels editing mode and restores the original value.
/// </summary>
/// <returns>True if operation was successful. False otherwise.</returns>
public bool CancelEdit()
{
return CancelEdit(DataGridEditingUnit.Row);
}
/// <summary>
/// Cancels editing mode for the specified DataGridEditingUnit and restores its original value.
/// </summary>
/// <param name="editingUnit">Specifies whether to cancel edit for a Cell or Row.</param>
/// <returns>True if operation was successful. False otherwise.</returns>
public bool CancelEdit(DataGridEditingUnit editingUnit)
{
return CancelEdit(editingUnit, raiseEvents: true);
}
/// <summary>
/// Commits editing mode and pushes changes to the backend.
/// </summary>
/// <returns>True if operation was successful. False otherwise.</returns>
public bool CommitEdit()
{
return CommitEdit(DataGridEditingUnit.Row, true);
}
/// <summary>
/// Commits editing mode for the specified DataGridEditingUnit and pushes changes to the backend.
/// </summary>
/// <param name="editingUnit">Specifies whether to commit edit for a Cell or Row.</param>
/// <param name="exitEditingMode">Editing mode is left if True.</param>
/// <returns>True if operation was successful. False otherwise.</returns>
public bool CommitEdit(DataGridEditingUnit editingUnit, bool exitEditingMode)
{
if (!EndCellEdit(
editAction: DataGridEditAction.Commit,
exitEditingMode: editingUnit == DataGridEditingUnit.Cell ? exitEditingMode : true,
keepFocus: ContainsFocus,
raiseEvents: true))
{
return false;
}
if (editingUnit == DataGridEditingUnit.Row)
{
return EndRowEdit(DataGridEditAction.Commit, exitEditingMode, raiseEvents: true);
}
return true;
}
/// <summary>
/// Scrolls the specified item or RowGroupHeader and/or column into view.
/// If item is not null: scrolls the row representing the item into view;
/// If column is not null: scrolls the column into view;
/// If both item and column are null, the method returns without scrolling.
/// </summary>
/// <param name="item">an item from the DataGrid's items source or a CollectionViewGroup from the collection view</param>
/// <param name="column">a column from the DataGrid's columns collection</param>
public void ScrollIntoView(object item, DataGridColumn column)
{
if ((column == null && (item == null || FirstDisplayedNonFillerColumnIndex == -1))
|| (column != null && column.OwningGrid != this))
{
// no-op
return;
}
if (item == null)
{
// scroll column into view
ScrollSlotIntoView(
column.Index,
DisplayData.FirstScrollingSlot,
forCurrentCellChange: false,
forceHorizontalScroll: true);
}
else
{
int slot = -1;
DataGridRowGroupInfo rowGroupInfo = null;
if (item is DataGridCollectionViewGroup collectionViewGroup)
{
rowGroupInfo = RowGroupInfoFromCollectionViewGroup(collectionViewGroup);
if (rowGroupInfo == null)
{
Debug.Assert(false);
return;
}
slot = rowGroupInfo.Slot;
}
else
{
// the row index will be set to -1 if the item is null or not in the list
int rowIndex = DataConnection.IndexOf(item);
if (rowIndex == -1)
{
return;
}
slot = SlotFromRowIndex(rowIndex);
}
int columnIndex = (column == null) ? FirstDisplayedNonFillerColumnIndex : column.Index;
if (_collapsedSlotsTable.Contains(slot))
{
// We need to expand all parent RowGroups so that the slot is visible
if (rowGroupInfo != null)
{
ExpandRowGroupParentChain(rowGroupInfo.Level - 1, rowGroupInfo.Slot);
}
else
{
rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(slot));
Debug.Assert(rowGroupInfo != null);
if (rowGroupInfo != null)
{
ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot);
}
}
// Update Scrollbar and display information
NegVerticalOffset = 0;
SetVerticalOffset(0);
ResetDisplayedRows();
DisplayData.FirstScrollingSlot = 0;
ComputeScrollBarsLayout();
}
ScrollSlotIntoView(
columnIndex, slot,
forCurrentCellChange: true,
forceHorizontalScroll: true);
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (DataConnection.DataSource != null && !DataConnection.EventsWired)
{
DataConnection.WireEvents(DataConnection.DataSource);
InitializeElements(true /*recycleRows*/);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
// When wired to INotifyCollectionChanged, the DataGrid will be cleaned up by GC
if (DataConnection.DataSource != null && DataConnection.EventsWired)
{
DataConnection.UnWireEvents(DataConnection.DataSource);
}
}
/// <summary>
/// Arranges the content of the <see cref="T:Avalonia.Controls.DataGridRow" />.
/// </summary>
/// <param name="finalSize">
/// The final area within the parent that this element should use to arrange itself and its children.
/// </param>
/// <returns>
/// The actual size used by the <see cref="T:Avalonia.Controls.DataGridRow" />.
/// </returns>
protected override Size ArrangeOverride(Size finalSize)
{
if (_makeFirstDisplayedCellCurrentCellPending)
{
MakeFirstDisplayedCellCurrentCell();
}
if (Bounds.Width != finalSize.Width)
{
// If our final width has changed, we might need to update the filler
InvalidateColumnHeadersArrange();
InvalidateCellsArrange();
}
return base.ArrangeOverride(finalSize);
}
/// <summary>
/// Measures the children of a <see cref="T:Avalonia.Controls.DataGridRow" /> to prepare for
/// arranging them during the
/// <see cref="M:Avalonia.Controls.DataGridRow.ArrangeOverride(System.Windows.Size)" /> pass.
/// </summary>
/// <returns>
/// The size that the <see cref="T:Avalonia.Controls.DataGridRow" /> determines it needs during layout, based on its calculations of child object allocated sizes.
/// </returns>
/// <param name="availableSize">
/// The available size that this element can give to child elements. Indicates an upper limit that
/// child elements should not exceed.
/// </param>
protected override Size MeasureOverride(Size availableSize)
{
// Delay layout until after the initial measure to avoid invalid calculations when the
// DataGrid is not part of the visual tree
if (!_measured)
{
_measured = true;
// We don't need to clear the rows because it was already done when the ItemsSource changed
RefreshRowsAndColumns(clearRows: false);
//// Update our estimates now that the DataGrid has all of the information necessary
UpdateRowDetailsHeightEstimate();
// Update frozen columns to account for columns added prior to loading or autogenerated columns
if (FrozenColumnCountWithFiller > 0)
{
ProcessFrozenColumnCount();
}
}
Size desiredSize;
// This is a shortcut to skip layout if we don't have any columns
if (ColumnsInternal.VisibleEdgedColumnsWidth == 0)
{
if (_hScrollBar != null && _hScrollBar.IsVisible)
{
_hScrollBar.IsVisible = false;
}
if (_vScrollBar != null && _vScrollBar.IsVisible)
{
_vScrollBar.IsVisible = false;
}
desiredSize = base.MeasureOverride(availableSize);
}
else
{
if (_rowsPresenter != null)
{
_rowsPresenter.InvalidateMeasure();
}
InvalidateColumnHeadersMeasure();
desiredSize = base.MeasureOverride(availableSize);
ComputeScrollBarsLayout();
}
return desiredSize;
}
/// <inheritdoc/>
protected override void OnDataContextBeginUpdate()
{
base.OnDataContextBeginUpdate();
NotifyDataContextPropertyForAllRowCells(GetAllRows(), true);
}
/// <inheritdoc/>
protected override void OnDataContextEndUpdate()
{
base.OnDataContextEndUpdate();
NotifyDataContextPropertyForAllRowCells(GetAllRows(), false);
}
/// <summary>
/// Raises the BeginningEdit event.
/// </summary>
protected virtual void OnBeginningEdit(DataGridBeginningEditEventArgs e)
{
BeginningEdit?.Invoke(this, e);
}
/// <summary>
/// Raises the CellEditEnded event.
/// </summary>
protected virtual void OnCellEditEnded(DataGridCellEditEndedEventArgs e)
{
CellEditEnded?.Invoke(this, e);
}
/// <summary>
/// Raises the CellEditEnding event.
/// </summary>
protected virtual void OnCellEditEnding(DataGridCellEditEndingEventArgs e)
{
CellEditEnding?.Invoke(this, e);
}
/// <summary>
/// Raises the CellPointerPressed event.
/// </summary>
internal virtual void OnCellPointerPressed(DataGridCellPointerPressedEventArgs e)
{
CellPointerPressed?.Invoke(this, e);
}
/// <summary>
/// Raises the CurrentCellChanged event.
/// </summary>
protected virtual void OnCurrentCellChanged(EventArgs e)
{
CurrentCellChanged?.Invoke(this, e);
}
/// <summary>
/// Raises the LoadingRow event for row preparation.
/// </summary>
protected virtual void OnLoadingRow(DataGridRowEventArgs e)
{
EventHandler<DataGridRowEventArgs> handler = LoadingRow;
if (handler != null)
{
Debug.Assert(!_loadedRows.Contains(e.Row));
_loadedRows.Add(e.Row);
LoadingOrUnloadingRow = true;
handler(this, e);
LoadingOrUnloadingRow = false;
Debug.Assert(_loadedRows.Contains(e.Row));
_loadedRows.Remove(e.Row);
}
}
/// <summary>
/// Scrolls the DataGrid according to the direction of the delta.
/// </summary>
/// <param name="e">PointerWheelEventArgs</param>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
var delta = e.Delta;
// KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform.
// If Shift-Key is pressed and X is close to 0 we swap the Vector.
if (e.KeyModifiers == KeyModifiers.Shift && MathUtilities.IsZero(delta.X))
{
delta = new Vector(delta.Y, delta.X);
}
if(UpdateScroll(delta * DATAGRID_mouseWheelDelta))
{
e.Handled = true;
}
else
{
e.Handled = e.Handled || !ScrollViewer.GetIsScrollChainingEnabled(this);
}
}
internal bool UpdateScroll(Vector delta)
{
if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0)
{
var handled = false;
var ignoreInvalidate = false;
var scrollHeight = 0d;
// Vertical scroll handling
if (delta.Y > 0)
{
scrollHeight = Math.Max(-_verticalOffset, -delta.Y);
}
else if (delta.Y < 0)
{
if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible)
{
scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), -delta.Y);
}
else
{
double maximum = EdgedRowsHeightCalculated - CellsEstimatedHeight;
scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), -delta.Y);
}
}
if (scrollHeight != 0)
{
DisplayData.PendingVerticalScrollHeight = scrollHeight;
handled = true;
}
// Horizontal scroll handling
if (delta.X != 0)
{
var horizontalOffset = HorizontalOffset - delta.X;
var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth);
if (horizontalOffset < 0)
{
horizontalOffset = 0;
}
if (horizontalOffset > widthNotVisible)
{
horizontalOffset = widthNotVisible;
}
if (UpdateHorizontalOffset(horizontalOffset))
{
// We don't need to invalidate once again after UpdateHorizontalOffset.
ignoreInvalidate = true;
handled = true;
}
}
if (handled)
{
if (!ignoreInvalidate)
{
InvalidateRowsMeasure(invalidateIndividualElements: false);
}
return true;
}
}
return false;
}
/// <summary>
/// Raises the PreparingCellForEdit event.
/// </summary>
protected virtual void OnPreparingCellForEdit(DataGridPreparingCellForEditEventArgs e)
{
PreparingCellForEdit?.Invoke(this, e);
}
/// <summary>
/// Raises the RowEditEnded event.
/// </summary>
protected virtual void OnRowEditEnded(DataGridRowEditEndedEventArgs e)
{
RowEditEnded?.Invoke(this, e);
}
/// <summary>
/// Raises the RowEditEnding event.
/// </summary>
protected virtual void OnRowEditEnding(DataGridRowEditEndingEventArgs e)
{
RowEditEnding?.Invoke(this, e);
}
/// <summary>
/// Raises the SelectionChanged event and clears the _selectionChanged.
/// This event won't get raised again until after _selectionChanged is set back to true.
/// </summary>
protected virtual void OnSelectionChanged(SelectionChangedEventArgs e)
{
RaiseEvent(e);
}
/// <summary>
/// Raises the UnloadingRow event for row recycling.
/// </summary>
protected virtual void OnUnloadingRow(DataGridRowEventArgs e)
{
EventHandler<DataGridRowEventArgs> handler = UnloadingRow;
if (handler != null)
{
LoadingOrUnloadingRow = true;
handler(this, e);
LoadingOrUnloadingRow = false;
}
}
/// <summary>
/// Comparator class so we can sort list by the display index
/// </summary>
public class DisplayIndexComparer : IComparer<DataGridColumn>
{
int IComparer<DataGridColumn>.Compare(DataGridColumn x, DataGridColumn y)
{
return (x.DisplayIndexWithFiller < y.DisplayIndexWithFiller) ? -1 : 1;
}
}
/// <summary>
/// Builds the visual tree for the column header when a new template is applied.
/// </summary>
//TODO Validation UI
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
// The template has changed, so we need to refresh the visuals
_measured = false;
if (_columnHeadersPresenter != null)
{
// If we're applying a new template, we want to remove the old column headers first
_columnHeadersPresenter.Children.Clear();
}
_columnHeadersPresenter = e.NameScope.Find<DataGridColumnHeadersPresenter>(DATAGRID_elementColumnHeadersPresenterName);
if (_columnHeadersPresenter != null)
{
if (ColumnsInternal.FillerColumn != null)
{
ColumnsInternal.FillerColumn.IsRepresented = false;
}
_columnHeadersPresenter.OwningGrid = this;
// Columns were added before our Template was applied, add the ColumnHeaders now
List<DataGridColumn> sortedInternal = new List<DataGridColumn>(ColumnsItemsInternal);
sortedInternal.Sort(new DisplayIndexComparer());
foreach (DataGridColumn column in sortedInternal)
{
InsertDisplayedColumnHeader(column);
}
}
if (_rowsPresenter != null)
{
// If we're applying a new template, we want to remove the old rows first
UnloadElements(recycle: false);
}
_rowsPresenter = e.NameScope.Find<DataGridRowsPresenter>(DATAGRID_elementRowsPresenterName);
if (_rowsPresenter != null)
{
_rowsPresenter.OwningGrid = this;
InvalidateRowHeightEstimate();
UpdateRowDetailsHeightEstimate();
}
_frozenColumnScrollBarSpacer = e.NameScope.Find<Control>(DATAGRID_elementFrozenColumnScrollBarSpacerName);
if (_hScrollBar != null)
{
_hScrollBar.Scroll -= HorizontalScrollBar_Scroll;
}
_hScrollBar = e.NameScope.Find<ScrollBar>(DATAGRID_elementHorizontalScrollbarName);
if (_hScrollBar != null)
{
_hScrollBar.IsTabStop = false;
_hScrollBar.Maximum = 0.0;
_hScrollBar.Orientation = Orientation.Horizontal;
_hScrollBar.IsVisible = false;
_hScrollBar.Scroll += HorizontalScrollBar_Scroll;
}
if (_vScrollBar != null)
{
_vScrollBar.Scroll -= VerticalScrollBar_Scroll;
}
_vScrollBar = e.NameScope.Find<ScrollBar>(DATAGRID_elementVerticalScrollbarName);
if (_vScrollBar != null)
{
_vScrollBar.IsTabStop = false;
_vScrollBar.Maximum = 0.0;
_vScrollBar.Orientation = Orientation.Vertical;
_vScrollBar.IsVisible = false;
_vScrollBar.Scroll += VerticalScrollBar_Scroll;
}
_topLeftCornerHeader = e.NameScope.Find<ContentControl>(DATAGRID_elementTopLeftCornerHeaderName);
EnsureTopLeftCornerHeader(); // EnsureTopLeftCornerHeader checks for a null _topLeftCornerHeader;
_topRightCornerHeader = e.NameScope.Find<ContentControl>(DATAGRID_elementTopRightCornerHeaderName);
_bottomRightCorner = e.NameScope.Find<Visual>(DATAGRID_elementBottomRightCornerHeaderName);
}
/// <summary>
/// Cancels editing mode for the specified DataGridEditingUnit and restores its original value.
/// </summary>
/// <param name="editingUnit">Specifies whether to cancel edit for a Cell or Row.</param>
/// <param name="raiseEvents">Specifies whether or not to raise editing events</param>
/// <returns>True if operation was successful. False otherwise.</returns>
internal bool CancelEdit(DataGridEditingUnit editingUnit, bool raiseEvents)
{
if (!EndCellEdit(
DataGridEditAction.Cancel,
exitEditingMode: true,
keepFocus: ContainsFocus,
raiseEvents: raiseEvents))
{
return false;
}
if (editingUnit == DataGridEditingUnit.Row)
{
return EndRowEdit(DataGridEditAction.Cancel, true, raiseEvents);
}
return true;
}
/// <summary>
/// call when: selection changes or SelectedItems object changes
/// </summary>
internal void CoerceSelectedItem()
{
object selectedItem = null;
if (SelectionMode == DataGridSelectionMode.Extended &&
CurrentSlot != -1 &&
_selectedItems.ContainsSlot(CurrentSlot))
{
selectedItem = CurrentItem;
}
else if (_selectedItems.Count > 0)
{
selectedItem = _selectedItems[0];
}
SetValueNoCallback(SelectedItemProperty, selectedItem);
// Update the SelectedIndex
int newIndex = -1;
if (selectedItem != null)
{
newIndex = DataConnection.IndexOf(selectedItem);
}
SetValueNoCallback(SelectedIndexProperty, newIndex);
}
internal static DataGridCell GetOwningCell(Control element)
{
Debug.Assert(element != null);
DataGridCell cell = element as DataGridCell;
while (element != null && cell == null)
{
element = element.Parent as Control;
cell = element as DataGridCell;
}
return cell;
}
internal IEnumerable<object> GetSelectionInclusive(int startRowIndex, int endRowIndex)
{
int endSlot = SlotFromRowIndex(endRowIndex);
foreach (int slot in _selectedItems.GetSlots(SlotFromRowIndex(startRowIndex)))
{
if (slot > endSlot)
{
break;
}
yield return DataConnection.GetDataItem(RowIndexFromSlot(slot));
}
}
internal void InitializeElements(bool recycleRows)
{
try
{
_noCurrentCellChangeCount++;
// The underlying collection has changed and our editing row (if there is one)
// is no longer relevant, so we should force a cancel edit.
CancelEdit(DataGridEditingUnit.Row, raiseEvents: false);
// We want to persist selection throughout a reset, so store away the selected items
List<object> selectedItemsCache = new List<object>(_selectedItems.SelectedItemsCache);
if (recycleRows)
{
RefreshRows(recycleRows, clearRows: true);
}
else
{
RefreshRowsAndColumns(clearRows: true);
}
// Re-select the old items
_selectedItems.SelectedItemsCache = selectedItemsCache;
CoerceSelectedItem();
if (RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.Collapsed)
{
UpdateRowDetailsVisibilityMode(RowDetailsVisibilityMode);
}
// The currently displayed rows may have incorrect visual states because of the selection change
ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
}
finally
{
NoCurrentCellChangeCount--;
}
}
internal bool IsDoubleClickRecordsClickOnCall(Control element)
{
if (_clickedElement == element)
{
_clickedElement = null;
return true;
}
else
{
_clickedElement = element;
return false;
}
}
// Returns the item or the CollectionViewGroup that is used as the DataContext for a given slot.
// If the DataContext is an item, rowIndex is set to the index of the item within the collection
internal object ItemFromSlot(int slot, ref int rowIndex)
{
if (RowGroupHeadersTable.Contains(slot))
{
return RowGroupHeadersTable.GetValueAt(slot)?.CollectionViewGroup;
}
else
{
rowIndex = RowIndexFromSlot(slot);
return DataConnection.GetDataItem(rowIndex);
}
}
internal bool ProcessDownKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessDownKeyInternal(shift, ctrl);
}
internal bool ProcessEndKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessEndKey(shift, ctrl);
}
internal bool ProcessEnterKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessEnterKey(shift, ctrl);
}
internal bool ProcessHomeKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessHomeKey(shift, ctrl);
}
internal void ProcessHorizontalScroll(ScrollEventType scrollEventType)
{
if (_horizontalScrollChangesIgnored > 0)
{
return;
}
// If the user scrolls with the buttons, we need to update the new value of the scroll bar since we delay
// this calculation. If they scroll in another other way, the scroll bar's correct value has already been set
double scrollBarValueDifference = 0;
if (scrollEventType == ScrollEventType.SmallIncrement)
{
scrollBarValueDifference = GetHorizontalSmallScrollIncrease();
}
else if (scrollEventType == ScrollEventType.SmallDecrement)
{
scrollBarValueDifference = -GetHorizontalSmallScrollDecrease();
}
_horizontalScrollChangesIgnored++;
try
{
if (scrollBarValueDifference != 0)
{
Debug.Assert(_horizontalOffset + scrollBarValueDifference >= 0);
_hScrollBar.Value = _horizontalOffset + scrollBarValueDifference;
}
UpdateHorizontalOffset(_hScrollBar.Value);
}
finally
{
_horizontalScrollChangesIgnored--;
}
}
internal bool ProcessLeftKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessLeftKey(shift, ctrl);
}
internal bool ProcessNextKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessNextKey(shift, ctrl);
}
internal bool ProcessPriorKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessPriorKey(shift, ctrl);
}
internal bool ProcessRightKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessRightKey(shift, ctrl);
}
/// <summary>
/// Selects items and updates currency based on parameters
/// </summary>
/// <param name="columnIndex">column index to make current</param>
/// <param name="item">data item or CollectionViewGroup to make current</param>
/// <param name="backupSlot">slot to use in case the item is no longer valid</param>
/// <param name="action">selection action to perform</param>
/// <param name="scrollIntoView">whether or not the new current item should be scrolled into view</param>
internal void ProcessSelectionAndCurrency(int columnIndex, object item, int backupSlot, DataGridSelectionAction action, bool scrollIntoView)
{
_noSelectionChangeCount++;
_noCurrentCellChangeCount++;
try
{
int slot = -1;
if (item is DataGridCollectionViewGroup group)
{
DataGridRowGroupInfo groupInfo = RowGroupInfoFromCollectionViewGroup(group);
if (groupInfo != null)
{
slot = groupInfo.Slot;
}
}
else
{
slot = SlotFromRowIndex(DataConnection.IndexOf(item));
}
if (slot == -1)
{
slot = backupSlot;
}
if (slot < 0 || slot > SlotCount)
{
return;
}
switch (action)
{
case DataGridSelectionAction.AddCurrentToSelection:
SetRowSelection(slot, isSelected: true, setAnchorSlot: true);
break;
case DataGridSelectionAction.RemoveCurrentFromSelection:
SetRowSelection(slot, isSelected: false, setAnchorSlot: false);
break;
case DataGridSelectionAction.SelectFromAnchorToCurrent:
if (SelectionMode == DataGridSelectionMode.Extended && AnchorSlot != -1)
{
int anchorSlot = AnchorSlot;
if (slot <= anchorSlot)
{
SetRowsSelection(slot, anchorSlot);
}
else
{
SetRowsSelection(anchorSlot, slot);
}
}
else
{
goto case DataGridSelectionAction.SelectCurrent;
}
break;
case DataGridSelectionAction.SelectCurrent:
ClearRowSelection(slot, setAnchorSlot: true);
break;
case DataGridSelectionAction.None:
break;
}
if (CurrentSlot != slot || (CurrentColumnIndex != columnIndex && columnIndex != -1))
{
if (columnIndex == -1)
{
if (CurrentColumnIndex != -1)
{
columnIndex = CurrentColumnIndex;
}
else
{
DataGridColumn firstVisibleColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
if (firstVisibleColumn != null)
{
columnIndex = firstVisibleColumn.Index;
}
}
}
if (columnIndex != -1)
{
if (!SetCurrentCellCore(
columnIndex, slot,
commitEdit: true,
endRowEdit: SlotFromRowIndex(SelectedIndex) != slot)
|| (scrollIntoView &&
!ScrollSlotIntoView(
columnIndex, slot,
forCurrentCellChange: true,
forceHorizontalScroll: false)))
{
return;
}
}
}
_successfullyUpdatedSelection = true;
}
finally
{
NoCurrentCellChangeCount--;
NoSelectionChangeCount--;
}
}
internal bool ProcessUpKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessUpKey(shift, ctrl);
}
//internal void ProcessVerticalScroll(double oldValue, double newValue)
internal void ProcessVerticalScroll(ScrollEventType scrollEventType)
{
if (_verticalScrollChangesIgnored > 0)
{
return;
}
Debug.Assert(MathUtilities.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum));
_verticalScrollChangesIgnored++;
try
{
Debug.Assert(_vScrollBar != null);
if (scrollEventType == ScrollEventType.SmallIncrement)
{
DisplayData.PendingVerticalScrollHeight = GetVerticalSmallScrollIncrease();
double newVerticalOffset = _verticalOffset + DisplayData.PendingVerticalScrollHeight;
if (newVerticalOffset > _vScrollBar.Maximum)
{
DisplayData.PendingVerticalScrollHeight -= newVerticalOffset - _vScrollBar.Maximum;
}
}
else if (scrollEventType == ScrollEventType.SmallDecrement)
{
if (MathUtilities.GreaterThan(NegVerticalOffset, 0))
{
DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset;
}
else
{
int previousScrollingSlot = GetPreviousVisibleSlot(DisplayData.FirstScrollingSlot);
if (previousScrollingSlot >= 0)
{
ScrollSlotIntoView(previousScrollingSlot, scrolledHorizontally: false);
}
return;
}
}
else
{
DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset;
}
if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight))
{
// Invalidate so the scroll happens on idle
InvalidateRowsMeasure(invalidateIndividualElements: false);
}
}
finally
{
_verticalScrollChangesIgnored--;
}
}
internal void RefreshRowsAndColumns(bool clearRows)
{
if (_measured)
{
try
{
_noCurrentCellChangeCount++;
if (clearRows)
{
ClearRows(false);
ClearRowGroupHeadersTable();
PopulateRowGroupHeadersTable();
}
if (AutoGenerateColumns)
{
//Column auto-generation refreshes the rows too
AutoGenerateColumnsPrivate();
}
foreach (DataGridColumn column in ColumnsItemsInternal)
{
//We don't need to refresh the state of AutoGenerated column headers because they're up-to-date
if (!column.IsAutoGenerated && column.HasHeaderCell)
{
column.HeaderCell.UpdatePseudoClasses();
}
}
RefreshRows(recycleRows: false, clearRows: false);
if (Columns.Count > 0 && CurrentColumnIndex == -1)
{
MakeFirstDisplayedCellCurrentCell();
}
else
{
_makeFirstDisplayedCellCurrentCellPending = false;
_desiredCurrentColumnIndex = -1;
FlushCurrentCellChanged();
}
}
finally
{
NoCurrentCellChangeCount--;
}
}
else
{
if (clearRows)
{
ClearRows(recycle: false);
}
ClearRowGroupHeadersTable();
PopulateRowGroupHeadersTable();
}
}
internal bool ScrollSlotIntoView(int columnIndex, int slot, bool forCurrentCellChange, bool forceHorizontalScroll)
{
Debug.Assert(columnIndex >= 0 && columnIndex < ColumnsItemsInternal.Count);
Debug.Assert(DisplayData.FirstDisplayedScrollingCol >= -1 && DisplayData.FirstDisplayedScrollingCol < ColumnsItemsInternal.Count);
Debug.Assert(DisplayData.LastTotallyDisplayedScrollingCol >= -1 && DisplayData.LastTotallyDisplayedScrollingCol < ColumnsItemsInternal.Count);
Debug.Assert(!IsSlotOutOfBounds(slot));
Debug.Assert(DisplayData.FirstScrollingSlot >= -1 && DisplayData.FirstScrollingSlot < SlotCount);
Debug.Assert(ColumnsItemsInternal[columnIndex].IsVisible);
if (CurrentColumnIndex >= 0 &&
(CurrentColumnIndex != columnIndex || CurrentSlot != slot))
{
if (!CommitEditForOperation(columnIndex, slot, forCurrentCellChange) || IsInnerCellOutOfBounds(columnIndex, slot))
{
return false;
}
}
double oldHorizontalOffset = HorizontalOffset;
//scroll horizontally unless we're on a RowGroupHeader and we're not forcing horizontal scrolling
if ((forceHorizontalScroll || (slot != -1))
&& !ScrollColumnIntoView(columnIndex))
{
return false;
}
//scroll vertically
if (!ScrollSlotIntoView(slot, scrolledHorizontally: oldHorizontalOffset != HorizontalOffset))
{
return false;
}
return true;
}
// Convenient overload that commits the current edit.
internal bool SetCurrentCellCore(int columnIndex, int slot)
{
return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true);
}
internal bool UpdateHorizontalOffset(double newValue)
{
if (HorizontalOffset != newValue)
{
HorizontalOffset = newValue;
InvalidateColumnHeadersMeasure();
InvalidateRowsMeasure(true);
return true;
}
return false;
}
internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView)
{
_successfullyUpdatedSelection = false;
_noSelectionChangeCount++;
_noCurrentCellChangeCount++;
try
{
if (ColumnsInternal.RowGroupSpacerColumn.IsRepresented &&
columnIndex == ColumnsInternal.RowGroupSpacerColumn.Index)
{
columnIndex = -1;
}
if (IsSlotOutOfSelectionBounds(slot) || (columnIndex != -1 && IsColumnOutOfBounds(columnIndex)))
{
return false;
}
int newCurrentPosition = -1;
object item = ItemFromSlot(slot, ref newCurrentPosition);
if (EditingRow != null && slot != EditingRow.Slot && !CommitEdit(DataGridEditingUnit.Row, true))
{
return false;
}
if (DataConnection.CollectionView != null &&
DataConnection.CollectionView.CurrentPosition != newCurrentPosition)
{
DataConnection.MoveCurrentTo(item, slot, columnIndex, action, scrollIntoView);
}
else
{
ProcessSelectionAndCurrency(columnIndex, item, slot, action, scrollIntoView);
}
}
finally
{
NoCurrentCellChangeCount--;
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
internal void UpdateStateOnCurrentChanged(object currentItem, int currentPosition)
{
if (currentItem == CurrentItem && currentItem == SelectedItem && currentPosition == SelectedIndex)
{
// The DataGrid's CurrentItem is already up-to-date, so we don't need to do anything
return;
}
int columnIndex = CurrentColumnIndex;
if (columnIndex == -1)
{
if (IsColumnOutOfBounds(_desiredCurrentColumnIndex) ||
(ColumnsInternal.RowGroupSpacerColumn.IsRepresented && _desiredCurrentColumnIndex == ColumnsInternal.RowGroupSpacerColumn.Index))
{
columnIndex = FirstDisplayedNonFillerColumnIndex;
}
else
{
columnIndex = _desiredCurrentColumnIndex;
}
}
_desiredCurrentColumnIndex = -1;
try
{
_noSelectionChangeCount++;
_noCurrentCellChangeCount++;
if (!CommitEdit())
{
CancelEdit(DataGridEditingUnit.Row, false);
}
ClearRowSelection(true);
if (currentItem == null)
{
SetCurrentCellCore(-1, -1);
}
else
{
int slot = SlotFromRowIndex(currentPosition);
ProcessSelectionAndCurrency(columnIndex, currentItem, slot, DataGridSelectionAction.SelectCurrent, false);
}
}
finally
{
NoCurrentCellChangeCount--;
NoSelectionChangeCount--;
}
}
//TODO: Ensure right button is checked for
internal bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit)
{
KeyboardHelper.GetMetaKeyState(this, pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift);
return UpdateStateOnMouseRightButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl);
}
//TODO: Ensure left button is checked for
internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit)
{
KeyboardHelper.GetMetaKeyState(this, pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift);
return UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl);
}
internal void UpdateVerticalScrollBar()
{
if (_vScrollBar != null && _vScrollBar.IsVisible)
{
double cellsHeight = CellsEstimatedHeight;
double edgedRowsHeightCalculated = EdgedRowsHeightCalculated;
UpdateVerticalScrollBar(
needVertScrollbar: edgedRowsHeightCalculated > cellsHeight,
forceVertScrollbar: VerticalScrollBarVisibility == ScrollBarVisibility.Visible,
totalVisibleHeight: edgedRowsHeightCalculated,
cellsHeight: cellsHeight);
}
}
/// <summary>
/// If the editing element has focus, this method will set focus to the DataGrid itself
/// in order to force the element to lose focus. It will then wait for the editing element's
/// LostFocus event, at which point it will perform the specified action.
///
/// NOTE: It is important to understand that the specified action will be performed when the editing
/// element loses focus only if this method returns true. If it returns false, then the action
/// will not be performed later on, and should instead be performed by the caller, if necessary.
/// </summary>
/// <param name="action">Action to perform after the editing element loses focus</param>
/// <returns>True if the editing element had focus and the action was cached away; false otherwise</returns>
//TODO TabStop
internal bool WaitForLostFocus(Action action)
{
if (EditingRow != null && EditingColumnIndex != -1 && !_executingLostFocusActions)
{
DataGridColumn editingColumn = ColumnsItemsInternal[EditingColumnIndex];
Control editingElement = editingColumn.GetCellContent(EditingRow);
if (editingElement != null && editingElement.ContainsChild(_focusedObject))
{
Debug.Assert(_lostFocusActions != null);
_lostFocusActions.Enqueue(action);
editingElement.LostFocus += EditingElement_LostFocus;
//IsTabStop = true;
Focus();
return true;
}
}
return false;
}
/// <summary>
/// Raises the LoadingRowDetails for row details preparation
/// </summary>
protected virtual void OnLoadingRowDetails(DataGridRowDetailsEventArgs e)
{
EventHandler<DataGridRowDetailsEventArgs> handler = LoadingRowDetails;
if (handler != null)
{
LoadingOrUnloadingRow = true;
handler(this, e);
LoadingOrUnloadingRow = false;
}
}
/// <summary>
/// Raises the UnloadingRowDetails event
/// </summary>
protected virtual void OnUnloadingRowDetails(DataGridRowDetailsEventArgs e)
{
EventHandler<DataGridRowDetailsEventArgs> handler = UnloadingRowDetails;
if (handler != null)
{
LoadingOrUnloadingRow = true;
handler(this, e);
LoadingOrUnloadingRow = false;
}
}
internal void OnRowDetailsChanged()
{
if (!_scrollingByHeight)
{
// Update layout when RowDetails are expanded or collapsed, just updating the vertical scroll bar is not enough
// since rows could be added or removed
InvalidateMeasure();
}
}
private static void NotifyDataContextPropertyForAllRowCells(IEnumerable<DataGridRow> rowSource, bool arg2)
{
foreach (DataGridRow row in rowSource)
{
foreach (DataGridCell cell in row.Cells)
{
if (cell.Content is StyledElement cellContent)
{
DataContextProperty.Notifying?.Invoke(cellContent, arg2);
}
}
}
}
private void UpdateRowDetailsVisibilityMode(DataGridRowDetailsVisibilityMode newDetailsMode)
{
int itemCount = DataConnection.Count;
if (_rowsPresenter != null && itemCount > 0)
{
bool newDetailsVisibility = false;
switch (newDetailsMode)
{
case DataGridRowDetailsVisibilityMode.Visible:
newDetailsVisibility = true;
_showDetailsTable.AddValues(0, itemCount, true);
break;
case DataGridRowDetailsVisibilityMode.Collapsed:
newDetailsVisibility = false;
_showDetailsTable.AddValues(0, itemCount, false);
break;
case DataGridRowDetailsVisibilityMode.VisibleWhenSelected:
_showDetailsTable.Clear();
break;
}
bool updated = false;
foreach (DataGridRow row in GetAllRows())
{
if (row.IsVisible)
{
if (newDetailsMode == DataGridRowDetailsVisibilityMode.VisibleWhenSelected)
{
// For VisibleWhenSelected, we need to calculate the value for each individual row
newDetailsVisibility = _selectedItems.ContainsSlot(row.Slot);
}
if (row.AreDetailsVisible != newDetailsVisibility)
{
updated = true;
row.SetDetailsVisibilityInternal(newDetailsVisibility, raiseNotification: true, animate: false);
}
}
}
if (updated)
{
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsEstimatedHeight);
InvalidateRowsMeasure(invalidateIndividualElements: false);
}
}
}
private void AddNewCellPrivate(DataGridRow row, DataGridColumn column)
{
DataGridCell newCell = new DataGridCell();
PopulateCellContent(
isCellEdited: false,
dataGridColumn: column,
dataGridRow: row,
dataGridCell: newCell);
if (row.OwningGrid != null)
{
newCell.OwningColumn = column;
newCell.IsVisible = column.IsVisible;
if (row.OwningGrid.CellTheme is {} cellTheme)
{
newCell.SetValue(ThemeProperty, cellTheme, BindingPriority.Template);
}
}
row.Cells.Insert(column.Index, newCell);
}
private bool BeginCellEdit(RoutedEventArgs editingEventArgs)
{
if (CurrentColumnIndex == -1 || !GetRowSelection(CurrentSlot))
{
return false;
}
Debug.Assert(CurrentColumnIndex >= 0);
Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(CurrentSlot >= -1);
Debug.Assert(CurrentSlot < SlotCount);
Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot);
Debug.Assert(!GetColumnEffectiveReadOnlyState(CurrentColumn));
Debug.Assert(CurrentColumn.IsVisible);
if (_editingColumnIndex != -1)
{
// Current cell is already in edit mode
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
return true;
}
// Get or generate the editing row if it doesn't exist
DataGridRow dataGridRow = EditingRow;
if (dataGridRow == null)
{
if (IsSlotVisible(CurrentSlot))
{
dataGridRow = DisplayData.GetDisplayedElement(CurrentSlot) as DataGridRow;
Debug.Assert(dataGridRow != null);
}
else
{
dataGridRow = GenerateRow(RowIndexFromSlot(CurrentSlot), CurrentSlot);
}
}
Debug.Assert(dataGridRow != null);
// Cache these to see if they change later
int currentRowIndex = CurrentSlot;
int currentColumnIndex = CurrentColumnIndex;
// Raise the BeginningEdit event
DataGridCell dataGridCell = dataGridRow.Cells[CurrentColumnIndex];
DataGridBeginningEditEventArgs e = new DataGridBeginningEditEventArgs(CurrentColumn, dataGridRow, editingEventArgs);
OnBeginningEdit(e);
if (e.Cancel
|| currentRowIndex != CurrentSlot
|| currentColumnIndex != CurrentColumnIndex
|| !GetRowSelection(CurrentSlot)
|| (EditingRow == null && !BeginRowEdit(dataGridRow)))
{
// If either BeginningEdit was canceled, currency/selection was changed in the event handler,
// or we failed opening the row for edit, then we can no longer continue BeginCellEdit
return false;
}
Debug.Assert(EditingRow != null);
Debug.Assert(EditingRow.Slot == CurrentSlot);
// Finally, we can prepare the cell for editing
_editingColumnIndex = CurrentColumnIndex;
_editingEventArgs = editingEventArgs;
EditingRow.Cells[CurrentColumnIndex].UpdatePseudoClasses();
PopulateCellContent(
isCellEdited: true,
dataGridColumn: CurrentColumn,
dataGridRow: dataGridRow,
dataGridCell: dataGridCell);
return true;
}
//TODO Validation
private bool BeginRowEdit(DataGridRow dataGridRow)
{
Debug.Assert(EditingRow == null);
Debug.Assert(dataGridRow != null);
Debug.Assert(CurrentSlot >= -1);
Debug.Assert(CurrentSlot < SlotCount);
if (DataConnection.BeginEdit(dataGridRow.DataContext))
{
EditingRow = dataGridRow;
GenerateEditingElements();
return true;
}
return false;
}
private bool CancelRowEdit(bool exitEditingMode)
{
if (EditingRow == null)
{
return true;
}
Debug.Assert(EditingRow != null && EditingRow.Index >= -1);
Debug.Assert(EditingRow.Slot < SlotCount);
Debug.Assert(CurrentColumn != null);
object dataItem = EditingRow.DataContext;
if (!DataConnection.CancelEdit(dataItem))
{
return false;
}
foreach (DataGridColumn column in Columns)
{
if (!exitEditingMode && column.Index == _editingColumnIndex && column is DataGridBoundColumn)
{
continue;
}
PopulateCellContent(
isCellEdited: !exitEditingMode && column.Index == _editingColumnIndex,
dataGridColumn: column,
dataGridRow: EditingRow,
dataGridCell: EditingRow.Cells[column.Index]);
}
return true;
}
private bool CommitEditForOperation(int columnIndex, int slot, bool forCurrentCellChange)
{
if (forCurrentCellChange)
{
if (!EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: true, raiseEvents: true))
{
return false;
}
if (CurrentSlot != slot &&
!EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true))
{
return false;
}
}
if (IsColumnOutOfBounds(columnIndex))
{
return false;
}
if (slot >= SlotCount)
{
// Current cell was reset because the commit deleted row(s).
// Since the user wants to change the current cell, we don't
// want to end up with no current cell. We pick the last row
// in the grid which may be the 'new row'.
int lastSlot = LastVisibleSlot;
if (forCurrentCellChange &&
CurrentColumnIndex == -1 &&
lastSlot != -1)
{
SetAndSelectCurrentCell(columnIndex, lastSlot, forceCurrentCellSelection: false);
}
// Interrupt operation because it has become invalid.
return false;
}
return true;
}
//TODO Validation
private bool CommitRowEdit(bool exitEditingMode)
{
if (EditingRow == null)
{
return true;
}
Debug.Assert(EditingRow != null && EditingRow.Index >= -1);
Debug.Assert(EditingRow.Slot < SlotCount);
//if (!ValidateEditingRow(scrollIntoView: true, wireEvents: false))
if (!EditingRow.IsValid)
{
return false;
}
DataConnection.EndEdit(EditingRow.DataContext);
if (!exitEditingMode)
{
DataConnection.BeginEdit(EditingRow.DataContext);
}
return true;
}
private void CompleteCellsCollection(DataGridRow dataGridRow)
{
Debug.Assert(dataGridRow != null);
int cellsInCollection = dataGridRow.Cells.Count;
if (ColumnsItemsInternal.Count > cellsInCollection)
{
for (int columnIndex = cellsInCollection; columnIndex < ColumnsItemsInternal.Count; columnIndex++)
{
AddNewCellPrivate(dataGridRow, ColumnsItemsInternal[columnIndex]);
}
}
}
private void ComputeScrollBarsLayout()
{
if (_ignoreNextScrollBarsLayout)
{
_ignoreNextScrollBarsLayout = false;
//
}
bool isHorizontalScrollBarOverCells = IsHorizontalScrollBarOverCells;
bool isVerticalScrollBarOverCells = IsVerticalScrollBarOverCells;
double cellsWidth = CellsWidth;
double cellsHeight = CellsEstimatedHeight;
bool allowHorizScrollbar = false;
bool forceHorizScrollbar = false;
double horizScrollBarHeight = 0;
if (_hScrollBar != null)
{
forceHorizScrollbar = HorizontalScrollBarVisibility == ScrollBarVisibility.Visible;
allowHorizScrollbar = forceHorizScrollbar || (ColumnsInternal.VisibleColumnCount > 0 &&
HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled &&
HorizontalScrollBarVisibility != ScrollBarVisibility.Hidden);
// Compensate if the horizontal scrollbar is already taking up space
if (!forceHorizScrollbar && _hScrollBar.IsVisible)
{
if (!isHorizontalScrollBarOverCells)
{
cellsHeight += _hScrollBar.DesiredSize.Height;
}
}
if (!isHorizontalScrollBarOverCells)
{
horizScrollBarHeight = _hScrollBar.Height + _hScrollBar.Margin.Top + _hScrollBar.Margin.Bottom;
}
}
bool allowVertScrollbar = false;
bool forceVertScrollbar = false;
double vertScrollBarWidth = 0;
if (_vScrollBar != null)
{
forceVertScrollbar = VerticalScrollBarVisibility == ScrollBarVisibility.Visible;
allowVertScrollbar = forceVertScrollbar || (ColumnsItemsInternal.Count > 0 &&
VerticalScrollBarVisibility != ScrollBarVisibility.Disabled &&
VerticalScrollBarVisibility != ScrollBarVisibility.Hidden);
// Compensate if the vertical scrollbar is already taking up space
if (!forceVertScrollbar && _vScrollBar.IsVisible)
{
if (!isVerticalScrollBarOverCells)
{
cellsWidth += _vScrollBar.DesiredSize.Width;
}
}
if (!isVerticalScrollBarOverCells)
{
vertScrollBarWidth = _vScrollBar.Width + _vScrollBar.Margin.Left + _vScrollBar.Margin.Right;
}
}
// Now cellsWidth is the width potentially available for displaying data cells.
// Now cellsHeight is the height potentially available for displaying data cells.
bool needHorizScrollbar = false;
bool needVertScrollbar = false;
double totalVisibleWidth = ColumnsInternal.VisibleEdgedColumnsWidth;
double totalVisibleFrozenWidth = ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth();
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsEstimatedHeight);
double totalVisibleHeight = EdgedRowsHeightCalculated;
if (!forceHorizScrollbar && !forceVertScrollbar)
{
bool needHorizScrollbarWithoutVertScrollbar = false;
if (allowHorizScrollbar &&
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight))
{
double oldDataHeight = cellsHeight;
cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0);
needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true;
if (vertScrollBarWidth > 0 &&
allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
{
// Would we still need a horizontal scrollbar without the vertical one?
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
needHorizScrollbar = MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth);
}
}
if (!needHorizScrollbar)
{
// Restore old data height because turns out a horizontal scroll bar wouldn't make sense
cellsHeight = oldDataHeight;
}
}
// Store the current FirstScrollingSlot because removing the horizontal scrollbar could scroll
// the DataGrid up; however, if we realize later that we need to keep the horizontal scrollbar
// then we should use the first slot stored here which is not scrolled.
int firstScrollingSlot = DisplayData.FirstScrollingSlot;
UpdateDisplayedRows(firstScrollingSlot, cellsHeight);
if (allowVertScrollbar &&
MathUtilities.GreaterThan(cellsHeight, 0) &&
MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
cellsWidth -= vertScrollBarWidth;
Debug.Assert(cellsWidth >= 0);
needVertScrollbar = true;
}
DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
// we compute the number of visible columns only after we set up the vertical scroll bar.
ComputeDisplayedColumns();
if ((vertScrollBarWidth > 0 || horizScrollBarHeight > 0) &&
allowHorizScrollbar &&
needVertScrollbar && !needHorizScrollbar &&
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight))
{
cellsWidth += vertScrollBarWidth;
cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0);
needVertScrollbar = false;
UpdateDisplayedRows(firstScrollingSlot, cellsHeight);
if (cellsHeight > 0 &&
vertScrollBarWidth <= cellsWidth &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
cellsWidth -= vertScrollBarWidth;
Debug.Assert(cellsWidth >= 0);
needVertScrollbar = true;
}
if (needVertScrollbar)
{
needHorizScrollbar = true;
}
else
{
needHorizScrollbar = needHorizScrollbarWithoutVertScrollbar;
}
}
}
else if (forceHorizScrollbar && !forceVertScrollbar)
{
if (allowVertScrollbar)
{
if (cellsHeight > 0 &&
MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
cellsWidth -= vertScrollBarWidth;
Debug.Assert(cellsWidth >= 0);
needVertScrollbar = true;
}
DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
ComputeDisplayedColumns();
}
needHorizScrollbar = totalVisibleWidth > cellsWidth && totalVisibleFrozenWidth < cellsWidth;
}
else if (!forceHorizScrollbar && forceVertScrollbar)
{
if (allowHorizScrollbar)
{
if (cellsWidth > 0 &&
MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight) &&
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth))
{
cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0);
needHorizScrollbar = true;
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
}
DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
ComputeDisplayedColumns();
}
needVertScrollbar = DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount;
}
else
{
Debug.Assert(forceHorizScrollbar && forceVertScrollbar);
Debug.Assert(allowHorizScrollbar && allowVertScrollbar);
DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn();
ComputeDisplayedColumns();
needVertScrollbar = DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount;
needHorizScrollbar = totalVisibleWidth > cellsWidth && totalVisibleFrozenWidth < cellsWidth;
}
UpdateHorizontalScrollBar(needHorizScrollbar, forceHorizScrollbar, totalVisibleWidth, totalVisibleFrozenWidth, cellsWidth);
UpdateVerticalScrollBar(needVertScrollbar, forceVertScrollbar, totalVisibleHeight, cellsHeight);
if (_topRightCornerHeader != null)
{
// Show the TopRightHeaderCell based on vertical ScrollBar visibility
if (AreColumnHeadersVisible &&
_vScrollBar != null && _vScrollBar.IsVisible)
{
_topRightCornerHeader.IsVisible = true;
}
else
{
_topRightCornerHeader.IsVisible = false;
}
}
if (_bottomRightCorner != null)
{
// Show the BottomRightCorner when both scrollbars are visible.
_bottomRightCorner.IsVisible =
_hScrollBar != null && _hScrollBar.IsVisible &&
_vScrollBar != null && _vScrollBar.IsVisible;
}
DisplayData.FullyRecycleElements();
}
/// <summary>
/// Handles the current editing element's LostFocus event by performing any actions that
/// were cached by the WaitForLostFocus method.
/// </summary>
/// <param name="sender">Editing element</param>
/// <param name="e">RoutedEventArgs</param>
private void EditingElement_LostFocus(object sender, RoutedEventArgs e)
{
if (sender is Control editingElement)
{
editingElement.LostFocus -= EditingElement_LostFocus;
if (EditingRow != null && _editingColumnIndex != -1)
{
FocusEditingCell(true);
}
Debug.Assert(_lostFocusActions != null);
try
{
_executingLostFocusActions = true;
while (_lostFocusActions.Count > 0)
{
_lostFocusActions.Dequeue()();
}
}
finally
{
_executingLostFocusActions = false;
}
}
}
// Makes sure horizontal layout is updated to reflect any changes that affect it
private void EnsureHorizontalLayout()
{
ColumnsInternal.EnsureVisibleEdgedColumnsWidth();
InvalidateColumnHeadersMeasure();
InvalidateRowsMeasure(true);
InvalidateMeasure();
}
private void EnsureRowHeaderWidth()
{
if (AreRowHeadersVisible)
{
if (AreColumnHeadersVisible)
{
EnsureTopLeftCornerHeader();
}
if (_rowsPresenter != null)
{
bool updated = false;
foreach (Control element in _rowsPresenter.Children)
{
if (element is DataGridRow row)
{
// If the RowHeader resulted in a different width the last time it was measured, we need
// to re-measure it
if (row.HeaderCell != null && row.HeaderCell.DesiredSize.Width != ActualRowHeaderWidth)
{
row.HeaderCell.InvalidateMeasure();
updated = true;
}
}
else if (element is DataGridRowGroupHeader groupHeader && groupHeader.HeaderCell != null && groupHeader.HeaderCell.DesiredSize.Width != ActualRowHeaderWidth)
{
groupHeader.HeaderCell.InvalidateMeasure();
updated = true;
}
}
if (updated)
{
// We need to update the width of the horizontal scrollbar if the rowHeaders' width actually changed
InvalidateMeasure();
}
}
}
}
private void EnsureRowsPresenterVisibility()
{
if (_rowsPresenter != null)
{
// RowCount doesn't need to be considered, doing so might cause extra Visibility changes
_rowsPresenter.IsVisible = (ColumnsInternal.FirstVisibleNonFillerColumn != null);
}
}
private void EnsureTopLeftCornerHeader()
{
if (_topLeftCornerHeader != null)
{
_topLeftCornerHeader.IsVisible = (HeadersVisibility == DataGridHeadersVisibility.All);
if (_topLeftCornerHeader.IsVisible)
{
if (!double.IsNaN(RowHeaderWidth))
{
// RowHeaderWidth is set explicitly so we should use that
_topLeftCornerHeader.Width = RowHeaderWidth;
}
else if (VisibleSlotCount > 0)
{
// RowHeaders AutoSize and we have at least 1 row so take the desired width
_topLeftCornerHeader.Width = RowHeadersDesiredWidth;
}
}
}
}
private void InvalidateCellsArrange()
{
foreach (DataGridRow row in GetAllRows())
{
row.InvalidateHorizontalArrange();
}
}
private void InvalidateColumnHeadersArrange()
{
if (_columnHeadersPresenter != null)
{
_columnHeadersPresenter.InvalidateArrange();
}
}
private void InvalidateColumnHeadersMeasure()
{
if (_columnHeadersPresenter != null)
{
EnsureColumnHeadersVisibility();
_columnHeadersPresenter.InvalidateMeasure();
}
}
private void InvalidateRowsArrange()
{
if (_rowsPresenter != null)
{
_rowsPresenter.InvalidateArrange();
}
}
private void InvalidateRowsMeasure(bool invalidateIndividualElements)
{
if (_rowsPresenter != null)
{
_rowsPresenter.InvalidateMeasure();
if (invalidateIndividualElements)
{
foreach (Control element in _rowsPresenter.Children)
{
element.InvalidateMeasure();
}
}
}
}
//TODO: Make override?
private void DataGrid_GotFocus(object sender, RoutedEventArgs e)
{
if (!ContainsFocus)
{
ContainsFocus = true;
ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot))
{
if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row)
{
row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
}
}
}
// Keep track of which row contains the newly focused element
DataGridRow focusedRow = null;
Visual focusedElement = e.Source as Visual;
_focusedObject = focusedElement;
while (focusedElement != null)
{
focusedRow = focusedElement as DataGridRow;
if (focusedRow != null && focusedRow.OwningGrid == this && _focusedRow != focusedRow)
{
ResetFocusedRow();
_focusedRow = focusedRow.IsVisible ? focusedRow : null;
break;
}
focusedElement = focusedElement.GetVisualParent();
}
}
//TODO: Check
private void DataGrid_IsEnabledChanged(AvaloniaPropertyChangedEventArgs e)
{
}
private void DataGrid_KeyDown(object sender, KeyEventArgs e)
{
if (!e.Handled)
{
e.Handled = ProcessDataGridKey(e);
}
}
private void DataGrid_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab && CurrentColumnIndex != -1 && e.Source == this)
{
bool success =
ScrollSlotIntoView(
CurrentColumnIndex, CurrentSlot,
forCurrentCellChange: false,
forceHorizontalScroll: true);
Debug.Assert(success);
if (CurrentColumnIndex != -1 && SelectedItem == null)
{
SetRowSelection(CurrentSlot, isSelected: true, setAnchorSlot: true);
}
}
}
//TODO: Make override?
private void DataGrid_LostFocus(object sender, RoutedEventArgs e)
{
_focusedObject = null;
if (ContainsFocus)
{
bool focusLeftDataGrid = true;
bool dataGridWillReceiveRoutedEvent = true;
Visual focusedObject = FocusManager.GetFocusManager(this)?.GetFocusedElement() as Visual;
DataGridColumn editingColumn = null;
while (focusedObject != null)
{
if (focusedObject == this)
{
focusLeftDataGrid = false;
break;
}
// Walk up the visual tree. If we hit the root, try using the framework element's
// parent. We do this because Popups behave differently with respect to the visual tree,
// and it could have a parent even if the VisualTreeHelper doesn't find it.
var parent = focusedObject.Parent as Visual;
if (parent == null)
{
parent = focusedObject.GetVisualParent();
}
else
{
dataGridWillReceiveRoutedEvent = false;
}
focusedObject = parent;
}
if (EditingRow != null && EditingColumnIndex != -1)
{
editingColumn = ColumnsItemsInternal[EditingColumnIndex];
if (focusLeftDataGrid && editingColumn is DataGridTemplateColumn)
{
dataGridWillReceiveRoutedEvent = false;
}
}
if (focusLeftDataGrid && !(editingColumn is DataGridTemplateColumn))
{
ContainsFocus = false;
if (EditingRow != null)
{
CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true);
}
ResetFocusedRow();
ApplyDisplayedRowsState(DisplayData.FirstScrollingSlot, DisplayData.LastScrollingSlot);
if (CurrentColumnIndex != -1 && IsSlotVisible(CurrentSlot))
{
if (DisplayData.GetDisplayedElement(CurrentSlot) is DataGridRow row)
{
row.Cells[CurrentColumnIndex].UpdatePseudoClasses();
}
}
}
else if (!dataGridWillReceiveRoutedEvent)
{
if (focusedObject is Control focusedElement)
{
focusedElement.LostFocus += ExternalEditingElement_LostFocus;
}
}
}
}
private void EditingElement_Initialized(object sender, EventArgs e)
{
var element = sender as Control;
if (element != null)
{
element.Initialized -= EditingElement_Initialized;
}
PreparingCellForEditPrivate(element);
}
//TODO Validation
//TODO Binding
//TODO TabStop
private bool EndCellEdit(DataGridEditAction editAction, bool exitEditingMode, bool keepFocus, bool raiseEvents)
{
if (_editingColumnIndex == -1)
{
return true;
}
var editingRow = EditingRow;
if (editingRow is null)
{
return true;
}
Debug.Assert(_editingColumnIndex >= 0);
Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
// Cache these to see if they change later
int currentSlot = CurrentSlot;
int currentColumnIndex = CurrentColumnIndex;
// We're ready to start ending, so raise the event
DataGridCell editingCell = editingRow.Cells[_editingColumnIndex];
var editingElement = editingCell.Content as Control;
if (editingElement == null)
{
return false;
}
if (raiseEvents)
{
DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, editingRow, editingElement, editAction);
OnCellEditEnding(e);
if (e.Cancel)
{
// CellEditEnding has been cancelled
return false;
}
// Ensure that the current cell wasn't changed in the user's CellEditEnding handler
if (_editingColumnIndex == -1 ||
currentSlot != CurrentSlot ||
currentColumnIndex != CurrentColumnIndex)
{
return true;
}
Debug.Assert(EditingRow != null);
Debug.Assert(EditingRow.Slot == currentSlot);
Debug.Assert(_editingColumnIndex != -1);
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
}
// If we're canceling, let the editing column repopulate its old value if it wants
if (editAction == DataGridEditAction.Cancel)
{
CurrentColumn.CancelCellEditInternal(editingElement, _uneditedValue);
// Ensure that the current cell wasn't changed in the user column's CancelCellEdit
if (_editingColumnIndex == -1 ||
currentSlot != CurrentSlot ||
currentColumnIndex != CurrentColumnIndex)
{
return true;
}
Debug.Assert(EditingRow != null);
Debug.Assert(EditingRow.Slot == currentSlot);
Debug.Assert(_editingColumnIndex != -1);
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
}
// If we're committing, explicitly update the source but watch out for any validation errors
if (editAction == DataGridEditAction.Commit)
{
void SetValidationStatus(ICellEditBinding binding)
{
if (binding.IsValid)
{
ResetValidationStatus();
if (editingElement != null)
{
DataValidationErrors.ClearErrors(editingElement);
}
}
else
{
if (editingRow != null)
{
if (editingCell.IsValid)
{
editingCell.IsValid = false;
editingCell.UpdatePseudoClasses();
}
if (editingRow.IsValid)
{
editingRow.IsValid = false;
editingRow.ApplyState();
}
}
if (editingElement != null)
{
DataValidationErrors.SetError(editingElement,
new AggregateException(binding.ValidationErrors));
}
}
}
var editBinding = CurrentColumn?.CellEditBinding;
if (editBinding != null && !editBinding.CommitEdit())
{
SetValidationStatus(editBinding);
_validationSubscription?.Dispose();
_validationSubscription = editBinding.ValidationChanged.Subscribe(v => SetValidationStatus(editBinding));
ScrollSlotIntoView(CurrentColumnIndex, CurrentSlot, forCurrentCellChange: false, forceHorizontalScroll: true);
return false;
}
}
ResetValidationStatus();
if (exitEditingMode)
{
CurrentColumn.EndCellEditInternal();
_editingColumnIndex = -1;
editingCell.UpdatePseudoClasses();
//IsTabStop = true;
if (keepFocus && editingElement.ContainsFocusedElement())
{
Focus();
}
PopulateCellContent(
isCellEdited: !exitEditingMode,
dataGridColumn: CurrentColumn,
dataGridRow: editingRow,
dataGridCell: editingCell);
editingRow.InvalidateDesiredHeight();
var column = editingCell.OwningColumn;
if (column.Width.IsSizeToCells || column.Width.IsAuto)
{// Invalidate desired width and force recalculation
column.SetWidthDesiredValue(0);
editingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
}
}
// We're done, so raise the CellEditEnded event
if (raiseEvents)
{
OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, editingRow, editAction));
}
// There's a chance that somebody reopened this cell for edit within the CellEditEnded handler,
// so we should return false if we were supposed to exit editing mode, but we didn't
return !(exitEditingMode && currentColumnIndex == _editingColumnIndex);
}
//TODO Validation
private bool EndRowEdit(DataGridEditAction editAction, bool exitEditingMode, bool raiseEvents)
{
if (EditingRow == null || DataConnection.CommittingEdit)
{
return true;
}
if (_editingColumnIndex != -1 || (editAction == DataGridEditAction.Cancel && raiseEvents &&
!((DataConnection.EditableCollectionView != null && DataConnection.EditableCollectionView.CanCancelEdit) || (EditingRow.DataContext is IEditableObject))))
{
// Ending the row edit will fail immediately under the following conditions:
// 1. We haven't ended the cell edit yet.
// 2. We're trying to cancel edit when the underlying DataType is not an IEditableObject,
// because we have no way to properly restore the old value. We will only allow this to occur
// if raiseEvents == false, which means we're internally forcing a cancel.
return false;
}
DataGridRow editingRow = EditingRow;
if (raiseEvents)
{
DataGridRowEditEndingEventArgs e = new DataGridRowEditEndingEventArgs(EditingRow, editAction);
OnRowEditEnding(e);
if (e.Cancel)
{
// RowEditEnding has been cancelled
return false;
}
// Editing states might have been changed in the RowEditEnding handlers
if (_editingColumnIndex != -1)
{
return false;
}
if (editingRow != EditingRow)
{
return true;
}
}
// Call the appropriate commit or cancel methods
if (editAction == DataGridEditAction.Commit)
{
if (!CommitRowEdit(exitEditingMode))
{
return false;
}
}
else
{
if (!CancelRowEdit(exitEditingMode) && raiseEvents)
{
// We failed to cancel edit so we should abort unless we're forcing a cancel
return false;
}
}
ResetValidationStatus();
// Update the previously edited row's state
if (exitEditingMode && editingRow == EditingRow)
{
RemoveEditingElements();
ResetEditingRow();
}
// Raise the RowEditEnded event
if (raiseEvents)
{
OnRowEditEnded(new DataGridRowEditEndedEventArgs(editingRow, editAction));
}
return true;
}
private void EnsureColumnHeadersVisibility()
{
if (_columnHeadersPresenter != null)
{
_columnHeadersPresenter.IsVisible = AreColumnHeadersVisible;
}
}
private void EnsureVerticalGridLines()
{
if (AreColumnHeadersVisible)
{
double totalColumnsWidth = 0;
foreach (DataGridColumn column in ColumnsInternal)
{
totalColumnsWidth += column.ActualWidth;
column.HeaderCell.AreSeparatorsVisible = (column != ColumnsInternal.LastVisibleColumn || totalColumnsWidth < CellsWidth);
}
}
foreach (DataGridRow row in GetAllRows())
{
row.EnsureGridLines();
}
}
/// <summary>
/// Exits editing mode without trying to commit or revert the editing, and
/// without repopulating the edited row's cell.
/// </summary>
//TODO TabStop
private void ExitEdit(bool keepFocus)
{
if (EditingRow == null || DataConnection.CommittingEdit)
{
Debug.Assert(_editingColumnIndex == -1);
return;
}
if (_editingColumnIndex != -1)
{
Debug.Assert(_editingColumnIndex >= 0);
Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
_editingColumnIndex = -1;
EditingRow.Cells[CurrentColumnIndex].UpdatePseudoClasses();
}
//IsTabStop = true;
if (IsSlotVisible(EditingRow.Slot))
{
EditingRow.ApplyState();
}
ResetEditingRow();
if (keepFocus)
{
Focus();
}
}
private void ExternalEditingElement_LostFocus(object sender, RoutedEventArgs e)
{
if (sender is Control element)
{
element.LostFocus -= ExternalEditingElement_LostFocus;
DataGrid_LostFocus(sender, e);
}
}
private void FlushCurrentCellChanged()
{
if (_makeFirstDisplayedCellCurrentCellPending)
{
return;
}
if (SelectionHasChanged)
{
// selection is changing, don't raise CurrentCellChanged until it's done
_flushCurrentCellChanged = true;
FlushSelectionChanged();
return;
}
// We don't want to expand all intermediate currency positions, so we only expand
// the last current item before we flush the event
if (_collapsedSlotsTable.Contains(CurrentSlot))
{
DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(CurrentSlot));
Debug.Assert(rowGroupInfo != null);
if (rowGroupInfo != null)
{
ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot);
}
}
if (CurrentColumn != _previousCurrentColumn
|| CurrentItem != _previousCurrentItem)
{
CoerceSelectedItem();
_previousCurrentColumn = CurrentColumn;
_previousCurrentItem = CurrentItem;
OnCurrentCellChanged(EventArgs.Empty);
}
_flushCurrentCellChanged = false;
}
private void FlushSelectionChanged()
{
if (SelectionHasChanged && _noSelectionChangeCount == 0 && !_makeFirstDisplayedCellCurrentCellPending)
{
CoerceSelectedItem();
if (NoCurrentCellChangeCount != 0)
{
// current cell is changing, don't raise SelectionChanged until it's done
return;
}
SelectionHasChanged = false;
if (_flushCurrentCellChanged)
{
FlushCurrentCellChanged();
}
SelectionChangedEventArgs e = _selectedItems.GetSelectionChangedEventArgs();
if (e.AddedItems.Count > 0 || e.RemovedItems.Count > 0)
{
OnSelectionChanged(e);
}
}
}
//TODO TabStop
private bool FocusEditingCell(bool setFocus)
{
Debug.Assert(CurrentColumnIndex >= 0);
Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(CurrentSlot >= -1);
Debug.Assert(CurrentSlot < SlotCount);
Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
Debug.Assert(_editingColumnIndex != -1);
//IsTabStop = false;
_focusEditingControl = false;
bool success = false;
DataGridCell dataGridCell = EditingRow.Cells[_editingColumnIndex];
if (setFocus)
{
if (dataGridCell.ContainsFocusedElement())
{
success = true;
}
else
{
dataGridCell.Focus();
success = dataGridCell.ContainsFocusedElement();
}
_focusEditingControl = !success;
}
return success;
}
// Calculates the amount to scroll for the ScrollLeft button
// This is a method rather than a property to emphasize a calculation
private double GetHorizontalSmallScrollDecrease()
{
// If the first column is covered up, scroll to the start of it when the user clicks the left button
if (_negHorizontalOffset > 0)
{
return _negHorizontalOffset;
}
else
{
// The entire first column is displayed, show the entire previous column when the user clicks
// the left button
DataGridColumn previousColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn(
ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]);
if (previousColumn != null)
{
return GetEdgedColumnWidth(previousColumn);
}
else
{
// There's no previous column so don't move
return 0;
}
}
}
// Calculates the amount to scroll for the ScrollRight button
// This is a method rather than a property to emphasize a calculation
private double GetHorizontalSmallScrollIncrease()
{
if (DisplayData.FirstDisplayedScrollingCol >= 0)
{
return GetEdgedColumnWidth(ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]) - _negHorizontalOffset;
}
return 0;
}
// Calculates the amount the ScrollDown button should scroll
// This is a method rather than a property to emphasize that calculations are taking place
private double GetVerticalSmallScrollIncrease()
{
if (DisplayData.FirstScrollingSlot >= 0)
{
return GetExactSlotElementHeight(DisplayData.FirstScrollingSlot) - NegVerticalOffset;
}
return 0;
}
private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e)
{
ProcessHorizontalScroll(e.ScrollEventType);
HorizontalScroll?.Invoke(sender, e);
}
private bool IsColumnOutOfBounds(int columnIndex)
{
return columnIndex >= ColumnsItemsInternal.Count || columnIndex < 0;
}
private bool IsInnerCellOutOfBounds(int columnIndex, int slot)
{
return IsColumnOutOfBounds(columnIndex) || IsSlotOutOfBounds(slot);
}
private bool IsInnerCellOutOfSelectionBounds(int columnIndex, int slot)
{
return IsColumnOutOfBounds(columnIndex) || IsSlotOutOfSelectionBounds(slot);
}
private bool IsSlotOutOfBounds(int slot)
{
return slot >= SlotCount || slot < -1 || _collapsedSlotsTable.Contains(slot);
}
private bool IsSlotOutOfSelectionBounds(int slot)
{
if (RowGroupHeadersTable.Contains(slot))
{
Debug.Assert(slot >= 0 && slot < SlotCount);
return false;
}
else
{
int rowIndex = RowIndexFromSlot(slot);
return rowIndex < 0 || rowIndex >= DataConnection.Count;
}
}
private void MakeFirstDisplayedCellCurrentCell()
{
if (CurrentColumnIndex != -1)
{
_makeFirstDisplayedCellCurrentCellPending = false;
_desiredCurrentColumnIndex = -1;
FlushCurrentCellChanged();
return;
}
if (SlotCount != SlotFromRowIndex(DataConnection.Count))
{
_makeFirstDisplayedCellCurrentCellPending = true;
return;
}
// No current cell, therefore no selection either - try to set the current cell to the
// ItemsSource's ICollectionView.CurrentItem if it exists, otherwise use the first displayed cell.
int slot = 0;
if (DataConnection.CollectionView != null)
{
if (DataConnection.CollectionView.IsCurrentBeforeFirst ||
DataConnection.CollectionView.IsCurrentAfterLast)
{
slot = RowGroupHeadersTable.Contains(0) ? 0 : -1;
}
else
{
slot = SlotFromRowIndex(DataConnection.CollectionView.CurrentPosition);
}
}
else
{
if (SelectedIndex == -1)
{
// Try to default to the first row
slot = SlotFromRowIndex(0);
if (!IsSlotVisible(slot))
{
slot = -1;
}
}
else
{
slot = SlotFromRowIndex(SelectedIndex);
}
}
int columnIndex = FirstDisplayedNonFillerColumnIndex;
if (_desiredCurrentColumnIndex >= 0 && _desiredCurrentColumnIndex < ColumnsItemsInternal.Count)
{
columnIndex = _desiredCurrentColumnIndex;
}
SetAndSelectCurrentCell(columnIndex, slot, forceCurrentCellSelection: false);
AnchorSlot = slot;
_makeFirstDisplayedCellCurrentCellPending = false;
_desiredCurrentColumnIndex = -1;
FlushCurrentCellChanged();
}
private void PopulateCellContent(bool isCellEdited,
DataGridColumn dataGridColumn,
DataGridRow dataGridRow,
DataGridCell dataGridCell)
{
Debug.Assert(dataGridColumn != null);
Debug.Assert(dataGridRow != null);
Debug.Assert(dataGridCell != null);
Control element = null;
DataGridBoundColumn dataGridBoundColumn = dataGridColumn as DataGridBoundColumn;
if (isCellEdited)
{
// Generate EditingElement and apply column style if available
element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext);
if (element != null)
{
dataGridCell.Content = element;
if (element.IsInitialized)
{
PreparingCellForEditPrivate(element as Control);
}
else
{
// Subscribe to the new element's events
element.Initialized += EditingElement_Initialized;
}
}
}
else
{
// Generate Element and apply column style if available
element = dataGridColumn.GenerateElementInternal(dataGridCell, dataGridRow.DataContext);
dataGridCell.Content = element;
}
}
private void PreparingCellForEditPrivate(Control editingElement)
{
if (_editingColumnIndex == -1 ||
CurrentColumnIndex == -1 ||
EditingRow.Cells[CurrentColumnIndex].Content != editingElement)
{
// The current cell has changed since the call to BeginCellEdit, so the fact
// that this element has loaded is no longer relevant
return;
}
Debug.Assert(EditingRow != null);
Debug.Assert(_editingColumnIndex >= 0);
Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
FocusEditingCell(setFocus: ContainsFocus || _focusEditingControl);
// Prepare the cell for editing and raise the PreparingCellForEdit event for all columns
DataGridColumn dataGridColumn = CurrentColumn;
_uneditedValue = dataGridColumn.PrepareCellForEditInternal(editingElement, _editingEventArgs);
OnPreparingCellForEdit(new DataGridPreparingCellForEditEventArgs(dataGridColumn, EditingRow, _editingEventArgs, editingElement));
}
private bool ProcessAKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift, out bool alt);
if (ctrl && !shift && !alt && SelectionMode == DataGridSelectionMode.Extended)
{
SelectAll();
return true;
}
return false;
}
//TODO TabStop
//TODO FlowDirection
private bool ProcessDataGridKey(KeyEventArgs e)
{
bool focusDataGrid = false;
switch (e.Key)
{
case Key.Tab:
return ProcessTabKey(e);
case Key.Up:
focusDataGrid = ProcessUpKey(e);
break;
case Key.Down:
focusDataGrid = ProcessDownKey(e);
break;
case Key.PageDown:
focusDataGrid = ProcessNextKey(e);
break;
case Key.PageUp:
focusDataGrid = ProcessPriorKey(e);
break;
case Key.Left:
focusDataGrid = ProcessLeftKey(e);
break;
case Key.Right:
focusDataGrid = ProcessRightKey(e);
break;
case Key.F2:
return ProcessF2Key(e);
case Key.Home:
focusDataGrid = ProcessHomeKey(e);
break;
case Key.End:
focusDataGrid = ProcessEndKey(e);
break;
case Key.Enter:
focusDataGrid = ProcessEnterKey(e);
break;
case Key.Escape:
return ProcessEscapeKey();
case Key.A:
return ProcessAKey(e);
case Key.C:
return ProcessCopyKey(e.KeyModifiers);
case Key.Insert:
return ProcessCopyKey(e.KeyModifiers);
}
if (focusDataGrid)
{
Focus();
}
return focusDataGrid;
}
private bool ProcessDownKeyInternal(bool shift, bool ctrl)
{
DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleColumn;
int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
int lastSlot = LastVisibleSlot;
if (firstVisibleColumnIndex == -1 || lastSlot == -1)
{
return false;
}
if (WaitForLostFocus(() => ProcessDownKeyInternal(shift, ctrl)))
{
return true;
}
int nextSlot = -1;
if (CurrentSlot != -1)
{
nextSlot = GetNextVisibleSlot(CurrentSlot);
if (nextSlot >= SlotCount)
{
nextSlot = -1;
}
}
_noSelectionChangeCount++;
try
{
int desiredSlot;
int columnIndex;
DataGridSelectionAction action;
if (CurrentColumnIndex == -1)
{
desiredSlot = FirstVisibleSlot;
columnIndex = firstVisibleColumnIndex;
action = DataGridSelectionAction.SelectCurrent;
}
else if (ctrl)
{
if (shift)
{
// Both Ctrl and Shift
desiredSlot = lastSlot;
columnIndex = CurrentColumnIndex;
action = (SelectionMode == DataGridSelectionMode.Extended)
? DataGridSelectionAction.SelectFromAnchorToCurrent
: DataGridSelectionAction.SelectCurrent;
}
else
{
// Ctrl without Shift
desiredSlot = lastSlot;
columnIndex = CurrentColumnIndex;
action = DataGridSelectionAction.SelectCurrent;
}
}
else
{
if (nextSlot == -1)
{
return true;
}
if (shift)
{
// Shift without Ctrl
desiredSlot = nextSlot;
columnIndex = CurrentColumnIndex;
action = DataGridSelectionAction.SelectFromAnchorToCurrent;
}
else
{
// Neither Ctrl nor Shift
desiredSlot = nextSlot;
columnIndex = CurrentColumnIndex;
action = DataGridSelectionAction.SelectCurrent;
}
}
UpdateSelectionAndCurrency(columnIndex, desiredSlot, action, scrollIntoView: true);
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
private bool ProcessEndKey(bool shift, bool ctrl)
{
DataGridColumn dataGridColumn = ColumnsInternal.LastVisibleColumn;
int lastVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
int firstVisibleSlot = FirstVisibleSlot;
int lastVisibleSlot = LastVisibleSlot;
if (lastVisibleColumnIndex == -1 || firstVisibleSlot == -1)
{
return false;
}
if (WaitForLostFocus(() => ProcessEndKey(shift, ctrl)))
{
return true;
}
_noSelectionChangeCount++;
try
{
if (!ctrl)
{
return ProcessRightMost(lastVisibleColumnIndex, firstVisibleSlot);
}
else
{
DataGridSelectionAction action = (shift && SelectionMode == DataGridSelectionMode.Extended)
? DataGridSelectionAction.SelectFromAnchorToCurrent
: DataGridSelectionAction.SelectCurrent;
UpdateSelectionAndCurrency(lastVisibleColumnIndex, lastVisibleSlot, action, scrollIntoView: true);
}
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
private bool ProcessEnterKey(bool shift, bool ctrl)
{
int oldCurrentSlot = CurrentSlot;
if (!ctrl)
{
// If Enter was used by a TextBox, we shouldn't handle the key
if (FocusManager.GetFocusManager(this)?.GetFocusedElement() is TextBox focusedTextBox
&& focusedTextBox.AcceptsReturn)
{
return false;
}
if (WaitForLostFocus(() => ProcessEnterKey(shift, ctrl)))
{
return true;
}
// Enter behaves like down arrow - it commits the potential editing and goes down one cell.
if (!ProcessDownKeyInternal(false, ctrl))
{
return false;
}
}
else if (WaitForLostFocus(() => ProcessEnterKey(shift, ctrl)))
{
return true;
}
// Try to commit the potential editing
if (oldCurrentSlot == CurrentSlot &&
EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: true, raiseEvents: true) &&
EditingRow != null)
{
EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true);
ScrollIntoView(CurrentItem, CurrentColumn);
}
return true;
}
private bool ProcessEscapeKey()
{
if (WaitForLostFocus(() => ProcessEscapeKey()))
{
return true;
}
if (_editingColumnIndex != -1)
{
// Revert the potential cell editing and exit cell editing.
EndCellEdit(DataGridEditAction.Cancel, exitEditingMode: true, keepFocus: true, raiseEvents: true);
return true;
}
else if (EditingRow != null)
{
// Revert the potential row editing and exit row editing.
EndRowEdit(DataGridEditAction.Cancel, exitEditingMode: true, raiseEvents: true);
return true;
}
return false;
}
private bool ProcessF2Key(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
if (!shift && !ctrl &&
_editingColumnIndex == -1 && CurrentColumnIndex != -1 && GetRowSelection(CurrentSlot) &&
!GetColumnEffectiveReadOnlyState(CurrentColumn))
{
if (ScrollSlotIntoView(CurrentColumnIndex, CurrentSlot, forCurrentCellChange: false, forceHorizontalScroll: true))
{
BeginCellEdit(e);
}
return true;
}
return false;
}
private bool ProcessHomeKey(bool shift, bool ctrl)
{
DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
int firstVisibleSlot = FirstVisibleSlot;
if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1)
{
return false;
}
if (WaitForLostFocus(() => ProcessHomeKey(shift, ctrl)))
{
return true;
}
_noSelectionChangeCount++;
try
{
if (!ctrl)
{
return ProcessLeftMost(firstVisibleColumnIndex, firstVisibleSlot);
}
else
{
DataGridSelectionAction action = (shift && SelectionMode == DataGridSelectionMode.Extended)
? DataGridSelectionAction.SelectFromAnchorToCurrent
: DataGridSelectionAction.SelectCurrent;
UpdateSelectionAndCurrency(firstVisibleColumnIndex, firstVisibleSlot, action, scrollIntoView: true);
}
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
private bool ProcessLeftKey(bool shift, bool ctrl)
{
DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
int firstVisibleSlot = FirstVisibleSlot;
if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1)
{
return false;
}
if (WaitForLostFocus(() => ProcessLeftKey(shift, ctrl)))
{
return true;
}
int previousVisibleColumnIndex = -1;
if (CurrentColumnIndex != -1)
{
dataGridColumn = ColumnsInternal.GetPreviousVisibleNonFillerColumn(ColumnsItemsInternal[CurrentColumnIndex]);
if (dataGridColumn != null)
{
previousVisibleColumnIndex = dataGridColumn.Index;
}
}
_noSelectionChangeCount++;
try
{
if (ctrl)
{
return ProcessLeftMost(firstVisibleColumnIndex, firstVisibleSlot);
}
else
{
if (RowGroupHeadersTable.Contains(CurrentSlot))
{
CollapseRowGroup(RowGroupHeadersTable.GetValueAt(CurrentSlot).CollectionViewGroup, collapseAllSubgroups: false);
}
else if (CurrentColumnIndex == -1)
{
UpdateSelectionAndCurrency(
firstVisibleColumnIndex,
firstVisibleSlot,
DataGridSelectionAction.SelectCurrent,
scrollIntoView: true);
}
else
{
if (previousVisibleColumnIndex == -1)
{
return true;
}
UpdateSelectionAndCurrency(
previousVisibleColumnIndex,
CurrentSlot,
DataGridSelectionAction.None,
scrollIntoView: true);
}
}
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
// Ctrl Left <==> Home
private bool ProcessLeftMost(int firstVisibleColumnIndex, int firstVisibleSlot)
{
_noSelectionChangeCount++;
try
{
int desiredSlot;
DataGridSelectionAction action;
if (CurrentColumnIndex == -1)
{
desiredSlot = firstVisibleSlot;
action = DataGridSelectionAction.SelectCurrent;
Debug.Assert(_selectedItems.Count == 0);
}
else
{
desiredSlot = CurrentSlot;
action = DataGridSelectionAction.None;
}
UpdateSelectionAndCurrency(firstVisibleColumnIndex, desiredSlot, action, scrollIntoView: true);
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
private bool ProcessNextKey(bool shift, bool ctrl)
{
DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
if (firstVisibleColumnIndex == -1 || DisplayData.FirstScrollingSlot == -1)
{
return false;
}
if (WaitForLostFocus(() => ProcessNextKey(shift, ctrl)))
{
return true;
}
int nextPageSlot = CurrentSlot == -1 ? DisplayData.FirstScrollingSlot : CurrentSlot;
Debug.Assert(nextPageSlot != -1);
int slot = GetNextVisibleSlot(nextPageSlot);
int scrollCount = DisplayData.NumTotallyDisplayedScrollingElements;
while (scrollCount > 0 && slot < SlotCount)
{
nextPageSlot = slot;
scrollCount--;
slot = GetNextVisibleSlot(slot);
}
_noSelectionChangeCount++;
try
{
DataGridSelectionAction action;
int columnIndex;
if (CurrentColumnIndex == -1)
{
columnIndex = firstVisibleColumnIndex;
action = DataGridSelectionAction.SelectCurrent;
}
else
{
columnIndex = CurrentColumnIndex;
action = (shift && SelectionMode == DataGridSelectionMode.Extended)
? action = DataGridSelectionAction.SelectFromAnchorToCurrent
: action = DataGridSelectionAction.SelectCurrent;
}
UpdateSelectionAndCurrency(columnIndex, nextPageSlot, action, scrollIntoView: true);
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
private bool ProcessPriorKey(bool shift, bool ctrl)
{
DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
if (firstVisibleColumnIndex == -1 || DisplayData.FirstScrollingSlot == -1)
{
return false;
}
if (WaitForLostFocus(() => ProcessPriorKey(shift, ctrl)))
{
return true;
}
int previousPageSlot = (CurrentSlot == -1) ? DisplayData.FirstScrollingSlot : CurrentSlot;
Debug.Assert(previousPageSlot != -1);
int scrollCount = DisplayData.NumTotallyDisplayedScrollingElements;
int slot = GetPreviousVisibleSlot(previousPageSlot);
while (scrollCount > 0 && slot != -1)
{
previousPageSlot = slot;
scrollCount--;
slot = GetPreviousVisibleSlot(slot);
}
Debug.Assert(previousPageSlot != -1);
_noSelectionChangeCount++;
try
{
int columnIndex;
DataGridSelectionAction action;
if (CurrentColumnIndex == -1)
{
columnIndex = firstVisibleColumnIndex;
action = DataGridSelectionAction.SelectCurrent;
}
else
{
columnIndex = CurrentColumnIndex;
action = (shift && SelectionMode == DataGridSelectionMode.Extended)
? DataGridSelectionAction.SelectFromAnchorToCurrent
: DataGridSelectionAction.SelectCurrent;
}
UpdateSelectionAndCurrency(columnIndex, previousPageSlot, action, scrollIntoView: true);
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
private bool ProcessRightKey(bool shift, bool ctrl)
{
DataGridColumn dataGridColumn = ColumnsInternal.LastVisibleColumn;
int lastVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
int firstVisibleSlot = FirstVisibleSlot;
if (lastVisibleColumnIndex == -1 || firstVisibleSlot == -1)
{
return false;
}
if (WaitForLostFocus(delegate { ProcessRightKey(shift, ctrl); }))
{
return true;
}
int nextVisibleColumnIndex = -1;
if (CurrentColumnIndex != -1)
{
dataGridColumn = ColumnsInternal.GetNextVisibleColumn(ColumnsItemsInternal[CurrentColumnIndex]);
if (dataGridColumn != null)
{
nextVisibleColumnIndex = dataGridColumn.Index;
}
}
_noSelectionChangeCount++;
try
{
if (ctrl)
{
return ProcessRightMost(lastVisibleColumnIndex, firstVisibleSlot);
}
else
{
if (RowGroupHeadersTable.Contains(CurrentSlot))
{
ExpandRowGroup(RowGroupHeadersTable.GetValueAt(CurrentSlot).CollectionViewGroup, expandAllSubgroups: false);
}
else if (CurrentColumnIndex == -1)
{
int firstVisibleColumnIndex = ColumnsInternal.FirstVisibleColumn == null ? -1 : ColumnsInternal.FirstVisibleColumn.Index;
UpdateSelectionAndCurrency(
firstVisibleColumnIndex,
firstVisibleSlot,
DataGridSelectionAction.SelectCurrent,
scrollIntoView: true);
}
else
{
if (nextVisibleColumnIndex == -1)
{
return true;
}
UpdateSelectionAndCurrency(
nextVisibleColumnIndex,
CurrentSlot,
DataGridSelectionAction.None,
scrollIntoView: true);
}
}
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
// Ctrl Right <==> End
private bool ProcessRightMost(int lastVisibleColumnIndex, int firstVisibleSlot)
{
_noSelectionChangeCount++;
try
{
int desiredSlot;
DataGridSelectionAction action;
if (CurrentColumnIndex == -1)
{
desiredSlot = firstVisibleSlot;
action = DataGridSelectionAction.SelectCurrent;
}
else
{
desiredSlot = CurrentSlot;
action = DataGridSelectionAction.None;
}
UpdateSelectionAndCurrency(lastVisibleColumnIndex, desiredSlot, action, scrollIntoView: true);
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
private bool ProcessTabKey(KeyEventArgs e)
{
KeyboardHelper.GetMetaKeyState(this, e.KeyModifiers, out bool ctrl, out bool shift);
return ProcessTabKey(e, shift, ctrl);
}
private bool ProcessTabKey(KeyEventArgs e, bool shift, bool ctrl)
{
if (ctrl || _editingColumnIndex == -1 || IsReadOnly)
{
//Go to the next/previous control on the page when
// - Ctrl key is used
// - Potential current cell is not edited, or the datagrid is read-only.
return false;
}
// Try to locate a writable cell before/after the current cell
Debug.Assert(CurrentColumnIndex != -1);
Debug.Assert(CurrentSlot != -1);
int neighborVisibleWritableColumnIndex, neighborSlot;
DataGridColumn dataGridColumn;
if (shift)
{
dataGridColumn = ColumnsInternal.GetPreviousVisibleWritableColumn(ColumnsItemsInternal[CurrentColumnIndex]);
neighborSlot = GetPreviousVisibleSlot(CurrentSlot);
if (EditingRow != null)
{
while (neighborSlot != -1 && RowGroupHeadersTable.Contains(neighborSlot))
{
neighborSlot = GetPreviousVisibleSlot(neighborSlot);
}
}
}
else
{
dataGridColumn = ColumnsInternal.GetNextVisibleWritableColumn(ColumnsItemsInternal[CurrentColumnIndex]);
neighborSlot = GetNextVisibleSlot(CurrentSlot);
if (EditingRow != null)
{
while (neighborSlot < SlotCount && RowGroupHeadersTable.Contains(neighborSlot))
{
neighborSlot = GetNextVisibleSlot(neighborSlot);
}
}
}
neighborVisibleWritableColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
if (neighborVisibleWritableColumnIndex == -1 && (neighborSlot == -1 || neighborSlot >= SlotCount))
{
// There is no previous/next row and no previous/next writable cell on the current row
return false;
}
if (WaitForLostFocus(() => ProcessTabKey(e, shift, ctrl)))
{
return true;
}
int targetSlot = -1, targetColumnIndex = -1;
_noSelectionChangeCount++;
try
{
if (neighborVisibleWritableColumnIndex == -1)
{
targetSlot = neighborSlot;
if (shift)
{
Debug.Assert(ColumnsInternal.LastVisibleWritableColumn != null);
targetColumnIndex = ColumnsInternal.LastVisibleWritableColumn.Index;
}
else
{
Debug.Assert(ColumnsInternal.FirstVisibleWritableColumn != null);
targetColumnIndex = ColumnsInternal.FirstVisibleWritableColumn.Index;
}
}
else
{
targetSlot = CurrentSlot;
targetColumnIndex = neighborVisibleWritableColumnIndex;
}
DataGridSelectionAction action;
if (targetSlot != CurrentSlot || (SelectionMode == DataGridSelectionMode.Extended))
{
if (IsSlotOutOfBounds(targetSlot))
{
return true;
}
action = DataGridSelectionAction.SelectCurrent;
}
else
{
action = DataGridSelectionAction.None;
}
UpdateSelectionAndCurrency(targetColumnIndex, targetSlot, action, scrollIntoView: true);
}
finally
{
NoSelectionChangeCount--;
}
if (_successfullyUpdatedSelection && !RowGroupHeadersTable.Contains(targetSlot))
{
BeginCellEdit(e);
}
// Return true to say we handled the key event even if the operation was unsuccessful. If we don't
// say we handled this event, the framework will continue to process the tab key and change focus.
return true;
}
private bool ProcessUpKey(bool shift, bool ctrl)
{
DataGridColumn dataGridColumn = ColumnsInternal.FirstVisibleNonFillerColumn;
int firstVisibleColumnIndex = (dataGridColumn == null) ? -1 : dataGridColumn.Index;
int firstVisibleSlot = FirstVisibleSlot;
if (firstVisibleColumnIndex == -1 || firstVisibleSlot == -1)
{
return false;
}
if (WaitForLostFocus(() => ProcessUpKey(shift, ctrl)))
{
return true;
}
int previousVisibleSlot = (CurrentSlot != -1) ? GetPreviousVisibleSlot(CurrentSlot) : -1;
_noSelectionChangeCount++;
try
{
int slot;
int columnIndex;
DataGridSelectionAction action;
if (CurrentColumnIndex == -1)
{
slot = firstVisibleSlot;
columnIndex = firstVisibleColumnIndex;
action = DataGridSelectionAction.SelectCurrent;
}
else if (ctrl)
{
if (shift)
{
// Both Ctrl and Shift
slot = firstVisibleSlot;
columnIndex = CurrentColumnIndex;
action = (SelectionMode == DataGridSelectionMode.Extended)
? DataGridSelectionAction.SelectFromAnchorToCurrent
: DataGridSelectionAction.SelectCurrent;
}
else
{
// Ctrl without Shift
slot = firstVisibleSlot;
columnIndex = CurrentColumnIndex;
action = DataGridSelectionAction.SelectCurrent;
}
}
else
{
if (previousVisibleSlot == -1)
{
return true;
}
if (shift)
{
// Shift without Ctrl
slot = previousVisibleSlot;
columnIndex = CurrentColumnIndex;
action = DataGridSelectionAction.SelectFromAnchorToCurrent;
}
else
{
// Neither Shift nor Ctrl
slot = previousVisibleSlot;
columnIndex = CurrentColumnIndex;
action = DataGridSelectionAction.SelectCurrent;
}
}
UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: true);
}
finally
{
NoSelectionChangeCount--;
}
return _successfullyUpdatedSelection;
}
private void RemoveDisplayedColumnHeader(DataGridColumn dataGridColumn)
{
if (_columnHeadersPresenter != null)
{
_columnHeadersPresenter.Children.Remove(dataGridColumn.HeaderCell);
}
}
private void RemoveDisplayedColumnHeaders()
{
if (_columnHeadersPresenter != null)
{
_columnHeadersPresenter.Children.Clear();
}
ColumnsInternal.FillerColumn.IsRepresented = false;
}
private bool ResetCurrentCellCore()
{
return (CurrentColumnIndex == -1 || SetCurrentCellCore(-1, -1));
}
private void ResetEditingRow()
{
if (EditingRow != null
&& EditingRow != _focusedRow
&& !IsSlotVisible(EditingRow.Slot))
{
// Unload the old editing row if it's off screen
EditingRow.Clip = null;
UnloadRow(EditingRow);
DisplayData.FullyRecycleElements();
}
EditingRow = null;
}
private void ResetFocusedRow()
{
if (_focusedRow != null
&& _focusedRow != EditingRow
&& !IsSlotVisible(_focusedRow.Slot))
{
// Unload the old focused row if it's off screen
_focusedRow.Clip = null;
UnloadRow(_focusedRow);
DisplayData.FullyRecycleElements();
}
_focusedRow = null;
}
public void SelectAll()
{
SetRowsSelection(0, SlotCount - 1);
}
private void SetAndSelectCurrentCell(int columnIndex,
int slot,
bool forceCurrentCellSelection)
{
DataGridSelectionAction action = forceCurrentCellSelection ? DataGridSelectionAction.SelectCurrent : DataGridSelectionAction.None;
UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: false);
}
// columnIndex = 2, rowIndex = -1 --> current cell belongs to the 'new row'.
// columnIndex = 2, rowIndex = 2 --> current cell is an inner cell
// columnIndex = -1, rowIndex = -1 --> current cell is reset
// columnIndex = -1, rowIndex = 2 --> Unexpected
private bool SetCurrentCellCore(int columnIndex, int slot, bool commitEdit, bool endRowEdit)
{
Debug.Assert(columnIndex < ColumnsItemsInternal.Count);
Debug.Assert(slot < SlotCount);
Debug.Assert(columnIndex == -1 || ColumnsItemsInternal[columnIndex].IsVisible);
Debug.Assert(!(columnIndex > -1 && slot == -1));
if (columnIndex == CurrentColumnIndex &&
slot == CurrentSlot)
{
Debug.Assert(DataConnection != null);
Debug.Assert(_editingColumnIndex == -1 || _editingColumnIndex == CurrentColumnIndex);
Debug.Assert(EditingRow == null || EditingRow.Slot == CurrentSlot || DataConnection.CommittingEdit);
return true;
}
Control oldDisplayedElement = null;
DataGridCellCoordinates oldCurrentCell = new DataGridCellCoordinates(CurrentCellCoordinates);
object newCurrentItem = null;
if (!RowGroupHeadersTable.Contains(slot))
{
int rowIndex = RowIndexFromSlot(slot);
if (rowIndex >= 0 && rowIndex < DataConnection.Count)
{
newCurrentItem = DataConnection.GetDataItem(rowIndex);
}
}
if (CurrentColumnIndex > -1)
{
Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(CurrentSlot < SlotCount);
if (!IsInnerCellOutOfBounds(oldCurrentCell.ColumnIndex, oldCurrentCell.Slot) &&
IsSlotVisible(oldCurrentCell.Slot))
{
oldDisplayedElement = DisplayData.GetDisplayedElement(oldCurrentCell.Slot);
}
if (!RowGroupHeadersTable.Contains(oldCurrentCell.Slot) && !_temporarilyResetCurrentCell)
{
bool keepFocus = ContainsFocus;
if (commitEdit)
{
if (!EndCellEdit(DataGridEditAction.Commit, exitEditingMode: true, keepFocus: keepFocus, raiseEvents: true))
{
return false;
}
// Resetting the current cell: setting it to (-1, -1) is not considered setting it out of bounds
if ((columnIndex != -1 && slot != -1 && IsInnerCellOutOfSelectionBounds(columnIndex, slot)) ||
IsInnerCellOutOfSelectionBounds(oldCurrentCell.ColumnIndex, oldCurrentCell.Slot))
{
return false;
}
if (endRowEdit && !EndRowEdit(DataGridEditAction.Commit, exitEditingMode: true, raiseEvents: true))
{
return false;
}
}
else
{
CancelEdit(DataGridEditingUnit.Row, false);
ExitEdit(keepFocus);
}
}
}
if (newCurrentItem != null)
{
slot = SlotFromRowIndex(DataConnection.IndexOf(newCurrentItem));
}
if (slot == -1 && columnIndex != -1)
{
return false;
}
CurrentColumnIndex = columnIndex;
CurrentSlot = slot;
if (_temporarilyResetCurrentCell)
{
if (columnIndex != -1)
{
_temporarilyResetCurrentCell = false;
}
}
if (!_temporarilyResetCurrentCell && _editingColumnIndex != -1)
{
_editingColumnIndex = columnIndex;
}
if (oldDisplayedElement != null)
{
if (oldDisplayedElement is DataGridRow row)
{
// Don't reset the state of the current cell if we're editing it because that would put it in an invalid state
UpdateCurrentState(oldDisplayedElement, oldCurrentCell.ColumnIndex, !(_temporarilyResetCurrentCell && row.IsEditing && _editingColumnIndex == oldCurrentCell.ColumnIndex));
}
else
{
UpdateCurrentState(oldDisplayedElement, oldCurrentCell.ColumnIndex, applyCellState: false);
}
}
if (CurrentColumnIndex > -1)
{
Debug.Assert(CurrentSlot > -1);
Debug.Assert(CurrentColumnIndex < ColumnsItemsInternal.Count);
Debug.Assert(CurrentSlot < SlotCount);
if (IsSlotVisible(CurrentSlot))
{
UpdateCurrentState(DisplayData.GetDisplayedElement(CurrentSlot), CurrentColumnIndex, applyCellState: true);
}
}
return true;
}
private void SetVerticalOffset(double newVerticalOffset)
{
_verticalOffset = newVerticalOffset;
if (_vScrollBar != null && !MathUtilities.AreClose(newVerticalOffset, _vScrollBar.Value))
{
_vScrollBar.Value = _verticalOffset;
}
}
private void UpdateCurrentState(Control displayedElement, int columnIndex, bool applyCellState)
{
if (displayedElement is DataGridRow row)
{
if (AreRowHeadersVisible)
{
row.ApplyHeaderStatus();
}
DataGridCell cell = row.Cells[columnIndex];
if (applyCellState)
{
cell.UpdatePseudoClasses();
}
}
else if (displayedElement is DataGridRowGroupHeader groupHeader)
{
groupHeader.UpdatePseudoClasses();
if (AreRowHeadersVisible)
{
groupHeader.ApplyHeaderStatus();
}
}
}
private void UpdateHorizontalScrollBar(bool needHorizScrollbar, bool forceHorizScrollbar, double totalVisibleWidth, double totalVisibleFrozenWidth, double cellsWidth)
{
if (_hScrollBar != null)
{
if (needHorizScrollbar || forceHorizScrollbar)
{
// viewportSize
// v---v
//|<|_____|###|>|
// ^ ^
// min max
// we want to make the relative size of the thumb reflect the relative size of the viewing area
// viewportSize / (max + viewportSize) = cellsWidth / max
// -> viewportSize = max * cellsWidth / (max - cellsWidth)
// always zero
_hScrollBar.Minimum = 0;
if (needHorizScrollbar)
{
// maximum travel distance -- not the total width
_hScrollBar.Maximum = totalVisibleWidth - cellsWidth;
Debug.Assert(totalVisibleFrozenWidth >= 0);
if (_frozenColumnScrollBarSpacer != null)
{
_frozenColumnScrollBarSpacer.Width = totalVisibleFrozenWidth;
}
Debug.Assert(_hScrollBar.Maximum >= 0);
// width of the scrollable viewing area
double viewPortSize = Math.Max(0, cellsWidth - totalVisibleFrozenWidth);
_hScrollBar.ViewportSize = viewPortSize;
_hScrollBar.LargeChange = viewPortSize;
// The ScrollBar should be in sync with HorizontalOffset at this point. There's a resize case
// where the ScrollBar will coerce an old value here, but we don't want that
if (_hScrollBar.Value != _horizontalOffset)
{
_hScrollBar.Value = _horizontalOffset;
}
_hScrollBar.IsEnabled = true;
}
else
{
_hScrollBar.Maximum = 0;
_hScrollBar.ViewportSize = 0;
_hScrollBar.IsEnabled = false;
}
if (!_hScrollBar.IsVisible)
{
// This will trigger a call to this method via Cells_SizeChanged for
_ignoreNextScrollBarsLayout = true;
// which no processing is needed.
_hScrollBar.IsVisible = true;
if (_hScrollBar.DesiredSize.Height == 0)
{
// We need to know the height for the rest of layout to work correctly so measure it now
_hScrollBar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
}
}
else
{
_hScrollBar.Maximum = 0;
if (_hScrollBar.IsVisible)
{
// This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_hScrollBar.IsVisible = false;
_ignoreNextScrollBarsLayout = true;
}
}
}
}
private void UpdateVerticalScrollBar(bool needVertScrollbar, bool forceVertScrollbar, double totalVisibleHeight, double cellsHeight)
{
if (_vScrollBar != null)
{
if (needVertScrollbar || forceVertScrollbar)
{
// viewportSize
// v---v
//|<|_____|###|>|
// ^ ^
// min max
// we want to make the relative size of the thumb reflect the relative size of the viewing area
// viewportSize / (max + viewportSize) = cellsWidth / max
// -> viewportSize = max * cellsHeight / (totalVisibleHeight - cellsHeight)
// -> = max * cellsHeight / (totalVisibleHeight - cellsHeight)
// -> = max * cellsHeight / max
// -> = cellsHeight
// always zero
_vScrollBar.Minimum = 0;
if (needVertScrollbar && !double.IsInfinity(cellsHeight))
{
// maximum travel distance -- not the total height
_vScrollBar.Maximum = totalVisibleHeight - cellsHeight;
Debug.Assert(_vScrollBar.Maximum >= 0);
// total height of the display area
_vScrollBar.ViewportSize = cellsHeight;
_vScrollBar.IsEnabled = true;
}
else
{
_vScrollBar.Maximum = 0;
_vScrollBar.ViewportSize = 0;
_vScrollBar.IsEnabled = false;
}
if (!_vScrollBar.IsVisible)
{
// This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_vScrollBar.IsVisible = true;
if (_vScrollBar.DesiredSize.Width == 0)
{
// We need to know the width for the rest of layout to work correctly so measure it now
_vScrollBar.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
_ignoreNextScrollBarsLayout = true;
}
}
else
{
_vScrollBar.Maximum = 0;
if (_vScrollBar.IsVisible)
{
// This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_vScrollBar.IsVisible = false;
_ignoreNextScrollBarsLayout = true;
}
}
}
}
private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e)
{
ProcessVerticalScroll(e.ScrollEventType);
VerticalScroll?.Invoke(sender, e);
}
//TODO: Ensure right button is checked for
private bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl)
{
Debug.Assert(slot >= 0);
if (shift || ctrl)
{
return true;
}
if (IsSlotOutOfBounds(slot))
{
return true;
}
if (GetRowSelection(slot))
{
return true;
}
// Unselect everything except the row that was clicked on
_noSelectionChangeCount++;
try
{
UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
}
finally
{
NoSelectionChangeCount--;
}
return true;
}
//TODO: Ensure left button is checked for
private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl)
{
bool beginEdit;
Debug.Assert(slot >= 0);
// Before changing selection, check if the current cell needs to be committed, and
// check if the current row needs to be committed. If any of those two operations are required and fail,
// do not change selection, and do not change current cell.
bool wasInEdit = EditingColumnIndex != -1;
if (IsSlotOutOfBounds(slot))
{
return true;
}
if (wasInEdit && (columnIndex != EditingColumnIndex || slot != CurrentSlot) &&
WaitForLostFocus(() => UpdateStateOnMouseLeftButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl)))
{
return true;
}
try
{
_noSelectionChangeCount++;
beginEdit = allowEdit &&
CurrentSlot == slot &&
columnIndex != -1 &&
(wasInEdit || CurrentColumnIndex == columnIndex) &&
!GetColumnEffectiveReadOnlyState(ColumnsItemsInternal[columnIndex]);
DataGridSelectionAction action;
if (SelectionMode == DataGridSelectionMode.Extended && shift)
{
// Shift select multiple rows
action = DataGridSelectionAction.SelectFromAnchorToCurrent;
}
else if (GetRowSelection(slot)) // Unselecting single row or Selecting a previously multi-selected row
{
if (!ctrl && SelectionMode == DataGridSelectionMode.Extended && _selectedItems.Count != 0)
{
// Unselect everything except the row that was clicked on
action = DataGridSelectionAction.SelectCurrent;
}
else if (ctrl && EditingRow == null)
{
action = DataGridSelectionAction.RemoveCurrentFromSelection;
}
else
{
action = DataGridSelectionAction.None;
}
}
else // Selecting a single row or multi-selecting with Ctrl
{
if (SelectionMode == DataGridSelectionMode.Single || !ctrl)
{
// Unselect the currently selected rows except the new selected row
action = DataGridSelectionAction.SelectCurrent;
}
else
{
action = DataGridSelectionAction.AddCurrentToSelection;
}
}
UpdateSelectionAndCurrency(columnIndex, slot, action, scrollIntoView: false);
}
finally
{
NoSelectionChangeCount--;
}
if (_successfullyUpdatedSelection && beginEdit && BeginCellEdit(pointerPressedEventArgs))
{
FocusEditingCell(setFocus: true);
}
return true;
}
/// <summary>
/// Returns the Group at the indicated level or null if the item is not in the ItemsSource
/// </summary>
/// <param name="item">item</param>
/// <param name="groupLevel">groupLevel</param>
/// <returns>The group the given item falls under or null if the item is not in the ItemsSource</returns>
public DataGridCollectionViewGroup GetGroupFromItem(object item, int groupLevel)
{
int itemIndex = DataConnection.IndexOf(item);
if (itemIndex == -1)
{
return null;
}
int groupHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(SlotFromRowIndex(itemIndex));
DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(groupHeaderSlot);
while (rowGroupInfo != null && rowGroupInfo.Level != groupLevel)
{
groupHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(rowGroupInfo.Slot);
rowGroupInfo = RowGroupHeadersTable.GetValueAt(groupHeaderSlot);
}
return rowGroupInfo?.CollectionViewGroup;
}
/// <summary>
/// Raises the LoadingRowGroup event
/// </summary>
/// <param name="e">EventArgs</param>
protected virtual void OnLoadingRowGroup(DataGridRowGroupHeaderEventArgs e)
{
EventHandler<DataGridRowGroupHeaderEventArgs> handler = LoadingRowGroup;
if (handler != null)
{
LoadingOrUnloadingRow = true;
handler(this, e);
LoadingOrUnloadingRow = false;
}
}
/// <summary>
/// Raises the UnLoadingRowGroup event
/// </summary>
/// <param name="e">EventArgs</param>
protected virtual void OnUnloadingRowGroup(DataGridRowGroupHeaderEventArgs e)
{
EventHandler<DataGridRowGroupHeaderEventArgs> handler = UnloadingRowGroup;
if (handler != null)
{
LoadingOrUnloadingRow = true;
handler(this, e);
LoadingOrUnloadingRow = false;
}
}
/// <summary>
/// Occurs before a DataGridRowGroupHeader header is used.
/// </summary>
public event EventHandler<DataGridRowGroupHeaderEventArgs> LoadingRowGroup;
/// <summary>
/// Occurs when the DataGridRowGroupHeader is available for reuse.
/// </summary>
public event EventHandler<DataGridRowGroupHeaderEventArgs> UnloadingRowGroup;
// Recursively expands parent RowGroupHeaders from the top down
private void ExpandRowGroupParentChain(int level, int slot)
{
if (level < 0)
{
return;
}
int previousHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(slot + 1);
DataGridRowGroupInfo rowGroupInfo = null;
while (previousHeaderSlot >= 0)
{
rowGroupInfo = RowGroupHeadersTable.GetValueAt(previousHeaderSlot);
Debug.Assert(rowGroupInfo != null);
if (level == rowGroupInfo.Level)
{
if (_collapsedSlotsTable.Contains(rowGroupInfo.Slot))
{
// Keep going up the chain
ExpandRowGroupParentChain(level - 1, rowGroupInfo.Slot - 1);
}
if (!rowGroupInfo.IsVisible)
{
EnsureRowGroupVisibility(rowGroupInfo, true, false);
}
return;
}
else
{
previousHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(previousHeaderSlot);
}
}
}
/// <summary>
/// This event is raised by OnCopyingRowClipboardContent method after the default row content is prepared.
/// Event listeners can modify or add to the row clipboard content.
/// </summary>
public event EventHandler<DataGridRowClipboardEventArgs> CopyingRowClipboardContent;
/// <summary>
/// This method raises the CopyingRowClipboardContent event.
/// </summary>
/// <param name="e">Contains the necessary information for generating the row clipboard content.</param>
protected virtual void OnCopyingRowClipboardContent(DataGridRowClipboardEventArgs e)
{
CopyingRowClipboardContent?.Invoke(this, e);
}
/// <summary>
/// This method formats a row (specified by a DataGridRowClipboardEventArgs) into
/// a single string to be added to the Clipboard when the DataGrid is copying its contents.
/// </summary>
/// <param name="e">DataGridRowClipboardEventArgs</param>
/// <returns>The formatted string.</returns>
private string FormatClipboardContent(DataGridRowClipboardEventArgs e)
{
var text = StringBuilderCache.Acquire();
var clipboardRowContent = e.ClipboardRowContent;
var numberOfItem = clipboardRowContent.Count;
for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++)
{
var cellContent = clipboardRowContent[cellIndex].Content?.ToString();
cellContent = cellContent?.Replace("\"", "\"\"");
text.Append($"\"{cellContent}\"");
if (cellIndex < numberOfItem - 1)
{
text.Append('\t');
}
else
{
text.Append('\r');
text.Append('\n');
}
}
return StringBuilderCache.GetStringAndRelease(text);
}
/// <summary>
/// Handles the case where a 'Copy' key ('C' or 'Insert') has been pressed. If pressed in combination with
/// the control key, and the necessary prerequisites are met, the DataGrid will copy its contents
/// to the Clipboard as text.
/// </summary>
/// <returns>Whether or not the DataGrid handled the key press.</returns>
private bool ProcessCopyKey(KeyModifiers modifiers)
{
KeyboardHelper.GetMetaKeyState(this, modifiers, out bool ctrl, out bool shift, out bool alt);
if (ctrl && !shift && !alt && ClipboardCopyMode != DataGridClipboardCopyMode.None && SelectedItems.Count > 0)
{
var textBuilder = StringBuilderCache.Acquire();
if (ClipboardCopyMode == DataGridClipboardCopyMode.IncludeHeader)
{
DataGridRowClipboardEventArgs headerArgs = new DataGridRowClipboardEventArgs(null, true);
foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns())
{
headerArgs.ClipboardRowContent.Add(new DataGridClipboardCellContent(null, column, column.Header));
}
OnCopyingRowClipboardContent(headerArgs);
textBuilder.Append(FormatClipboardContent(headerArgs));
}
for (int index = 0; index < SelectedItems.Count; index++)
{
object item = SelectedItems[index];
DataGridRowClipboardEventArgs itemArgs = new DataGridRowClipboardEventArgs(item, false);
foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns())
{
object content = column.GetCellValue(item, column.ClipboardContentBinding);
itemArgs.ClipboardRowContent.Add(new DataGridClipboardCellContent(item, column, content));
}
OnCopyingRowClipboardContent(itemArgs);
textBuilder.Append(FormatClipboardContent(itemArgs));
}
string text = StringBuilderCache.GetStringAndRelease(textBuilder);
if (!string.IsNullOrEmpty(text))
{
CopyToClipboard(text);
return true;
}
}
return false;
}
private async void CopyToClipboard(string text)
{
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard != null)
await clipboard.SetTextAsync(text);
}
/// <summary>
/// This is an empty content control that's used during the DataGrid's copy procedure
/// to determine the value of a ClipboardContentBinding for a particular column and item.
/// </summary>
internal ContentControl ClipboardContentControl
{
get
{
if (_clipboardContentControl == null)
{
_clipboardContentControl = new ContentControl();
}
return _clipboardContentControl;
}
}
//TODO Validation UI
private void ResetValidationStatus()
{
// Clear the invalid status of the Cell, Row and DataGrid
if (EditingRow != null)
{
EditingRow.IsValid = true;
if (EditingRow.Index != -1)
{
foreach (DataGridCell cell in EditingRow.Cells)
{
if (!cell.IsValid)
{
cell.IsValid = true;
cell.UpdatePseudoClasses();
}
}
EditingRow.ApplyState();
}
}
IsValid = true;
_validationSubscription?.Dispose();
_validationSubscription = null;
}
/// <summary>
/// Raises the AutoGeneratingColumn event.
/// </summary>
protected virtual void OnAutoGeneratingColumn(DataGridAutoGeneratingColumnEventArgs e)
{
AutoGeneratingColumn?.Invoke(this, e);
}
}
}