From f6f45687103d94b2baeec9bb88c2b239395c19ac Mon Sep 17 00:00:00 2001 From: CHINBOY Date: Wed, 28 May 2025 21:31:55 +0800 Subject: [PATCH] Add DataGrid --- .../DataGrid.Columns.cs | 1866 +++++++ .../DataGrid.Privates.cs | 427 +- src/AtomUI.Controls.DataGrid/DataGrid.Rows.cs | 3200 +++++++++++- src/AtomUI.Controls.DataGrid/DataGrid.cs | 4277 ++++++++++++++++- 4 files changed, 9587 insertions(+), 183 deletions(-) create mode 100644 src/AtomUI.Controls.DataGrid/DataGrid.Columns.cs diff --git a/src/AtomUI.Controls.DataGrid/DataGrid.Columns.cs b/src/AtomUI.Controls.DataGrid/DataGrid.Columns.cs new file mode 100644 index 000000000..adc155431 --- /dev/null +++ b/src/AtomUI.Controls.DataGrid/DataGrid.Columns.cs @@ -0,0 +1,1866 @@ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Reflection; +using AtomUI.Utils; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Utilities; + +namespace AtomUI.Controls; + +public partial class DataGrid +{ + #region 内部属性定义 + internal DataGridColumnHeadersPresenter? ColumnHeaders => _columnHeadersPresenter; + internal DataGridColumnCollection ColumnsInternal { get; } + internal List ColumnsItemsInternal => ColumnsInternal.ItemsInternal; + internal bool InDisplayIndexAdjustments { get; set; } + internal bool AreColumnHeadersVisible => (HeadersVisibility & DataGridHeadersVisibility.Column) == DataGridHeadersVisibility.Column; + + internal int CurrentColumnIndex + { + get => CurrentCellCoordinates.ColumnIndex; + private set => CurrentCellCoordinates.ColumnIndex = value; + } + + // the sum of the widths in pixels of the scrolling columns preceding + // the first displayed scrolling column + internal double HorizontalOffset + { + get => _horizontalOffset; + set + { + if (value < 0) + { + value = 0; + } + double widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth); + if (value > widthNotVisible) + { + value = widthNotVisible; + } + if (MathUtilities.AreClose(value, _horizontalOffset)) + { + return; + } + + if (_hScrollBar != null && !MathUtilities.AreClose(value, _hScrollBar.Value)) + { + _hScrollBar.Value = value; + } + _horizontalOffset = value; + + DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn(); + // update the lastTotallyDisplayedScrollingCol + ComputeDisplayedColumns(); + } + } + + private int FirstDisplayedNonFillerColumnIndex + { + get + { + DataGridColumn? column = ColumnsInternal.FirstVisibleNonFillerColumn; + if (column != null) + { + if (column.IsFrozen) + { + return column.Index; + } + if (DisplayData.FirstDisplayedScrollingCol >= column.Index) + { + return DisplayData.FirstDisplayedScrollingCol; + } + return column.Index; + } + return -1; + } + } + + internal int EditingColumnIndex + { + get; + private set; + } + + #endregion + + private DataGridColumnHeadersPresenter? _columnHeadersPresenter; + + protected virtual void NotifyColumnDisplayIndexChanged(DataGridColumnEventArgs e) + { + ColumnDisplayIndexChanged?.Invoke(this, e); + } + + protected internal virtual void NotifyColumnReordered(DataGridColumnEventArgs e) + { + EnsureVerticalGridLines(); + ColumnReordered?.Invoke(this, e); + } + + protected internal virtual void HandleColumnReordering(DataGridColumnReorderingEventArgs e) + { + ColumnReordering?.Invoke(this, e); + } + + protected internal virtual void HandleColumnSorting(DataGridColumnEventArgs e) + { + Sorting?.Invoke(this, e); + } + + /// + /// Adjusts the widths of all columns with DisplayIndex >= displayIndex such that the total + /// width is adjusted by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. + /// + /// Starting column DisplayIndex. + /// Adjustment amount (positive for increase, negative for decrease). + /// Whether or not this adjustment was initiated by a user action. + /// The remaining amount of adjustment. + internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated) + { + if (!MathUtilities.IsZero(amount)) + { + if (amount < 0) + { + amount = DecreaseColumnWidths(displayIndex, amount, userInitiated); + } + else + { + amount = IncreaseColumnWidths(displayIndex, amount, userInitiated); + } + } + return amount; + } + + /// + /// Grows an auto-column's width to the desired width. + /// + /// Auto-column to adjust. + /// The new desired width of the column. + internal void AutoSizeColumn(DataGridColumn column, double desiredWidth) + { + Debug.Assert(column.Width.IsAuto || column.Width.IsSizeToCells || column.Width.IsSizeToHeader || (!UsesStarSizing && column.Width.IsStar)); + + // If we're using star sizing and this is the first time we've measured this particular auto-column, + // we want to allow all rows to get measured before we setup the star widths. We won't know the final + // desired value of the column until all rows have been measured. Because of this, we wait until + // an Arrange occurs before we adjust star widths. + if (UsesStarSizing && !column.IsInitialDesiredWidthDetermined) + { + AutoSizingColumns = true; + } + + // Update the column's DesiredValue if it needs to grow to fit the new desired value + if (desiredWidth > column.Width.DesiredValue || double.IsNaN(column.Width.DesiredValue)) + { + // If this auto-growth occurs after the column's initial desired width has been determined, + // then the growth should act like a resize (squish columns to the right). Otherwise, if + // this column is newly added, we'll just set its display value directly. + if (UsesStarSizing && column.IsInitialDesiredWidthDetermined) + { + var oldWidth = column.Width; + column.Resize(oldWidth, + new(column.Width.Value, column.Width.UnitType, desiredWidth, desiredWidth), + false); + } + else + { + column.SetWidthInternalNoCallback(new DataGridLength(column.Width.Value, column.Width.UnitType, desiredWidth, desiredWidth)); + OnColumnWidthChanged(column); + } + } + } + + internal bool ColumnRequiresRightGridLine(DataGridColumn dataGridColumn, bool includeLastRightGridLineWhenPresent) + { + return (GridLinesVisibility == DataGridGridLinesVisibility.Vertical || GridLinesVisibility == DataGridGridLinesVisibility.All) && + VerticalGridLinesBrush != null && + (dataGridColumn != ColumnsInternal.LastVisibleColumn || + (includeLastRightGridLineWhenPresent && + ColumnsInternal.FillerColumn != null && + ColumnsInternal.FillerColumn.IsActive)); + } + + internal DataGridColumnCollection CreateColumnsInstance() + { + return new DataGridColumnCollection(this); + } + + /// + /// Decreases the widths of all columns with DisplayIndex >= displayIndex such that the total + /// width is decreased by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. + /// + /// Starting column DisplayIndex. + /// Amount to decrease (in pixels). + /// Whether or not this adjustment was initiated by a user action. + /// The remaining amount of adjustment. + internal double DecreaseColumnWidths(int displayIndex, double amount, bool userInitiated) + { + // 1. Take space from non-star columns with widths larger than desired widths (left to right). + amount = DecreaseNonStarColumnWidths(displayIndex, c => c.Width.DesiredValue, amount, false, false); + + // 2. Take space from star columns until they reach their min. + amount = AdjustStarColumnWidths(displayIndex, amount, userInitiated); + + // 3. Take space from non-star columns that have already been initialized, until they reach their min (right to left). + amount = DecreaseNonStarColumnWidths(displayIndex, c => c.ActualMinWidth, amount, true, false); + + // 4. Take space from all non-star columns until they reach their min, even if they are new (right to left). + amount = DecreaseNonStarColumnWidths(displayIndex, c => c.ActualMinWidth, amount, true, true); + + return amount; + } + + internal bool GetColumnReadOnlyState(DataGridColumn dataGridColumn, bool isReadOnly) + { + Debug.Assert(dataGridColumn != null); + + if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn && + dataGridBoundColumn.Binding is BindingBase binding) + { + var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString(); + + if (string.IsNullOrWhiteSpace(path)) + { + return true; + } + else + { + return DataConnection.GetPropertyIsReadOnly(path) || isReadOnly; + } + } + + return isReadOnly; + } + + // Returns the column's width + internal static double GetEdgedColumnWidth(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null); + return dataGridColumn.ActualWidth; + } + + /// + /// Increases the widths of all columns with DisplayIndex >= displayIndex such that the total + /// width is increased by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. + /// + /// Starting column DisplayIndex. + /// Amount of increase (in pixels). + /// Whether or not this adjustment was initiated by a user action. + /// The remaining amount of adjustment. + internal double IncreaseColumnWidths(int displayIndex, double amount, bool userInitiated) + { + // 1. Give space to non-star columns that are smaller than their desired widths (left to right). + amount = IncreaseNonStarColumnWidths(displayIndex, c => c.Width.DesiredValue, amount, false, false); + + // 2. Give space to star columns until they reach their max. + amount = AdjustStarColumnWidths(displayIndex, amount, userInitiated); + + // 3. Give space to non-star columns that have already been initialized, until they reach their max (right to left). + amount = IncreaseNonStarColumnWidths(displayIndex, c => c.ActualMaxWidth, amount, true, false); + + // 4. Give space to all non-star columns until they reach their max, even if they are new (right to left). + amount = IncreaseNonStarColumnWidths(displayIndex, c => c.ActualMaxWidth, amount, true, false); + + return amount; + } + + internal void OnClearingColumns() + { + // Rows need to be cleared first. There cannot be rows without also having columns. + ClearRows(false); + + // Removing all the column header cells + RemoveDisplayedColumnHeaders(); + + _horizontalOffset = _negHorizontalOffset = 0; + if (_hScrollBar != null && _hScrollBar.IsVisible) // + { + _hScrollBar.Value = 0; + } + } + + /// + /// Invalidates the widths of all columns because the resizing behavior of an individual column has changed. + /// + /// Column with CanUserResize property that has changed. + internal void OnColumnCanUserResizeChanged(DataGridColumn column) + { + if (column.IsVisible) + { + EnsureHorizontalLayout(); + } + } + + internal void OnColumnCollectionChanged_PostNotification(bool columnsGrew) + { + if (columnsGrew && + CurrentColumnIndex == -1) + { + MakeFirstDisplayedCellCurrentCell(); + } + + if (_autoGeneratingColumnOperationCount == 0) + { + EnsureRowsPresenterVisibility(); + InvalidateRowHeightEstimate(); + } + } + + internal void OnColumnCollectionChanged_PreNotification(bool columnsGrew) + { + // dataGridColumn==null means the collection was refreshed. + + if (columnsGrew && _autoGeneratingColumnOperationCount == 0 && ColumnsItemsInternal.Count == 1) + { + RefreshRows(false /*recycleRows*/, true /*clearRows*/); + } + else + { + InvalidateMeasure(); + } + } + + internal void NotifyColumnDisplayIndexChanged(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null); + DataGridColumnEventArgs e = new DataGridColumnEventArgs(dataGridColumn); + + // Call protected method to raise event + if (dataGridColumn != ColumnsInternal.RowGroupSpacerColumn) + { + NotifyColumnDisplayIndexChanged(e); + } + } + + internal void HandleColumnDisplayIndexChangedPostNotification() + { + // Notifications for adjusted display indexes. + FlushDisplayIndexChanged(true /*raiseEvent*/); + + // Our displayed columns may have changed so recompute them + UpdateDisplayedColumns(); + + // Invalidate layout + CorrectColumnFrozenStates(); + EnsureHorizontalLayout(); + } + + internal void OnColumnDisplayIndexChanging(DataGridColumn targetColumn, int newDisplayIndex) + { + Debug.Assert(targetColumn != null); + Debug.Assert(newDisplayIndex != targetColumn.DisplayIndexWithFiller); + + if (InDisplayIndexAdjustments) + { + // We are within columns display indexes adjustments. We do not allow changing display indexes while adjusting them. + throw DataGridError.DataGrid.CannotChangeColumnCollectionWhileAdjustingDisplayIndexes(); + } + + try + { + InDisplayIndexAdjustments = true; + + bool trackChange = targetColumn != ColumnsInternal.RowGroupSpacerColumn; + + DataGridColumn? column; + // Move is legal - let's adjust the affected display indexes. + if (newDisplayIndex < targetColumn.DisplayIndexWithFiller) + { + // DisplayIndex decreases. All columns with newDisplayIndex <= DisplayIndex < targetColumn.DisplayIndex + // get their DisplayIndex incremented. + for (int i = newDisplayIndex; i < targetColumn.DisplayIndexWithFiller; i++) + { + column = ColumnsInternal.GetColumnAtDisplayIndex(i); + Debug.Assert(column != null); + column.DisplayIndexWithFiller += 1; + if (trackChange) + { + column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on + } + } + } + else + { + // DisplayIndex increases. All columns with targetColumn.DisplayIndex < DisplayIndex <= newDisplayIndex + // get their DisplayIndex decremented. + for (int i = newDisplayIndex; i > targetColumn.DisplayIndexWithFiller; i--) + { + column = ColumnsInternal.GetColumnAtDisplayIndex(i); + Debug.Assert(column != null); + column.DisplayIndexWithFiller -= 1; + if (trackChange) + { + column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on + } + } + } + // Now let's actually change the order of the DisplayIndexMap + if (targetColumn.DisplayIndexWithFiller != -1) + { + ColumnsInternal.DisplayIndexMap.Remove(targetColumn.Index); + } + ColumnsInternal.DisplayIndexMap.Insert(newDisplayIndex, targetColumn.Index); + } + finally + { + InDisplayIndexAdjustments = false; + } + + // Note that displayIndex of moved column is updated by caller. + } + + internal void OnColumnBindingChanged(DataGridBoundColumn column) + { + // Update Binding in Displayed rows by regenerating the affected elements + if (_rowsPresenter != null) + { + foreach (DataGridRow row in GetAllRows()) + { + PopulateCellContent(false /*isCellEdited*/, column, row, row.Cells[column.Index]); + } + } + } + + /// + /// Adjusts the specified column's width according to its new maximum value. + /// + /// The column to adjust. + /// The old ActualMaxWidth of the column. + internal void OnColumnMaxWidthChanged(DataGridColumn column, double oldValue) + { + Debug.Assert(column != null); + + if (column.IsVisible && !MathUtils.AreClose(oldValue, column.ActualMaxWidth)) + { + DataGridLength oldWitdh = new(oldValue, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue); + if (column.ActualMaxWidth < column.Width.DisplayValue) + { + // If the maximum width has caused the column to decrease in size, try first to resize + // the columns to the right to make up for the difference in width, but don't limit the column's + // final display value to how much they could be resized. + AdjustColumnWidths(column.DisplayIndex + 1, column.Width.DisplayValue - column.ActualMaxWidth, false); + column.SetWidthDisplayValue(column.ActualMaxWidth); + } + else if (MathUtils.AreClose(column.Width.DisplayValue, oldValue) && column.Width.DesiredValue > column.Width.DisplayValue) + { + // If the column was previously limited by its maximum value but has more room now, + // attempt to resize the column to its desired width. + column.Resize(oldWitdh, + new (column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue), + false); + } + OnColumnWidthChanged(column); + } + } + + /// + /// Adjusts the specified column's width according to its new minimum value. + /// + /// The column to adjust. + /// The old ActualMinWidth of the column. + internal void OnColumnMinWidthChanged(DataGridColumn column, double oldValue) + { + Debug.Assert(column != null); + + if (column.IsVisible && !MathUtils.AreClose(oldValue, column.ActualMinWidth)) + { + DataGridLength oldWitdh = new(oldValue, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue); + if (column.ActualMinWidth > column.Width.DisplayValue) + { + // If the minimum width has caused the column to increase in size, try first to resize + // the columns to the right to make up for the difference in width, but don't limit the column's + // final display value to how much they could be resized. + AdjustColumnWidths(column.DisplayIndex + 1, column.Width.DisplayValue - column.ActualMinWidth, false); + column.SetWidthDisplayValue(column.ActualMinWidth); + } + else if (MathUtils.AreClose(column.Width.DisplayValue, oldValue) && column.Width.DesiredValue < column.Width.DisplayValue) + { + // If the column was previously limited by its minimum value but but can be smaller now, + // attempt to resize the column to its desired width. + column.Resize(oldWitdh, + new(column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue), + false); + } + OnColumnWidthChanged(column); + } + } + + internal void OnColumnReadOnlyStateChanging(DataGridColumn dataGridColumn, bool isReadOnly) + { + Debug.Assert(dataGridColumn != null); + if (isReadOnly && CurrentColumnIndex == dataGridColumn.Index) + { + // Edited column becomes read-only. Exit editing mode. + if (!EndCellEdit(DataGridEditAction.Commit, true /*exitEditingMode*/, ContainsFocus /*keepFocus*/, true /*raiseEvents*/)) + { + EndCellEdit(DataGridEditAction.Cancel, true /*exitEditingMode*/, ContainsFocus /*keepFocus*/, false /*raiseEvents*/); + } + } + } + + internal void OnColumnVisibleStateChanged(DataGridColumn updatedColumn) + { + Debug.Assert(updatedColumn != null); + + CorrectColumnFrozenStates(); + UpdateDisplayedColumns(); + EnsureRowsPresenterVisibility(); + EnsureHorizontalLayout(); + InvalidateColumnHeadersMeasure(); + + if (updatedColumn.IsVisible && + ColumnsInternal.VisibleColumnCount == 1 && CurrentColumnIndex == -1) + { + Debug.Assert(SelectedIndex == DataConnection.IndexOf(SelectedItem)); + if (SelectedIndex != -1) + { + SetAndSelectCurrentCell(updatedColumn.Index, SelectedIndex, true /*forceCurrentCellSelection*/); + } + else + { + MakeFirstDisplayedCellCurrentCell(); + } + } + + // We need to explicitly collapse the cells of the invisible column because layout only goes through + // visible ones + ColumnHeaders?.InvalidateChildIndex(); + foreach (var row in GetAllRows()) + { + row.Cells[updatedColumn.Index].IsVisible = updatedColumn.IsVisible; + row.InvalidateCellsIndex(); + } + } + + internal void OnColumnVisibleStateChanging(DataGridColumn targetColumn) + { + Debug.Assert(targetColumn != null); + + if (targetColumn.IsVisible && + CurrentColumn == targetColumn) + { + // Column of the current cell is made invisible. Trying to move the current cell to a neighbor column. May throw an exception. + DataGridColumn? dataGridColumn = ColumnsInternal.GetNextVisibleColumn(targetColumn); + if (dataGridColumn == null) + { + dataGridColumn = ColumnsInternal.GetPreviousVisibleNonFillerColumn(targetColumn); + } + if (dataGridColumn == null) + { + SetCurrentCellCore(-1, -1); + } + else + { + SetCurrentCellCore(dataGridColumn.Index, CurrentSlot); + } + } + } + + internal void OnColumnWidthChanged(DataGridColumn updatedColumn) + { + Debug.Assert(updatedColumn != null); + if (updatedColumn.IsVisible) + { + EnsureHorizontalLayout(); + } + } + + internal void OnFillerColumnWidthNeeded(double finalWidth) + { + DataGridFillerColumn? fillerColumn = ColumnsInternal.FillerColumn; + double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth + ActualRowHeaderWidth; + Debug.Assert(fillerColumn != null); + if (finalWidth - totalColumnsWidth > LayoutHelper.LayoutEpsilon) + { + fillerColumn.FillerWidth = finalWidth - totalColumnsWidth; + } + else + { + fillerColumn.FillerWidth = 0; + } + } + + internal void HandleInsertedColumnPostNotification(DataGridCellCoordinates newCurrentCellCoordinates, int newDisplayIndex) + { + // Update current cell if needed + if (newCurrentCellCoordinates.ColumnIndex != -1) + { + Debug.Assert(CurrentColumnIndex == -1); + SetAndSelectCurrentCell(newCurrentCellCoordinates.ColumnIndex, + newCurrentCellCoordinates.Slot, + ColumnsInternal.VisibleColumnCount == 1 /*forceCurrentCellSelection*/); + + if (newDisplayIndex < FrozenColumnCountWithFiller) + { + CorrectColumnFrozenStates(); + } + } + } + + internal void HandleInsertedColumnPreNotification(DataGridColumn insertedColumn) + { + // Fix the Index of all following columns + CorrectColumnIndexesAfterInsertion(insertedColumn, 1); + + Debug.Assert(insertedColumn.Index >= 0); + Debug.Assert(insertedColumn.Index < ColumnsItemsInternal.Count); + Debug.Assert(insertedColumn.OwningGrid == this); + + CorrectColumnDisplayIndexesAfterInsertion(insertedColumn); + + InsertDisplayedColumnHeader(insertedColumn); + + // Insert the missing data cells + if (SlotCount > 0) + { + int newColumnCount = ColumnsItemsInternal.Count; + + foreach (DataGridRow row in GetAllRows()) + { + if (row.Cells.Count < newColumnCount) + { + AddNewCellPrivate(row, insertedColumn); + } + } + } + + if (insertedColumn.IsVisible) + { + EnsureHorizontalLayout(); + } + + if (insertedColumn is DataGridBoundColumn boundColumn && !boundColumn.IsAutoGenerated) + { + boundColumn.SetHeaderFromBinding(); + } + } + + internal DataGridCellCoordinates NotifyInsertingColumn(int columnIndexInserted, DataGridColumn insertColumn) + { + DataGridCellCoordinates newCurrentCellCoordinates; + Debug.Assert(insertColumn != null); + + if (insertColumn.OwningGrid != null && insertColumn != ColumnsInternal.RowGroupSpacerColumn) + { + throw DataGridError.DataGrid.ColumnCannotBeReassignedToDifferentDataGrid(); + } + + // Reset current cell if there is one, no matter the relative position of the columns involved + if (CurrentColumnIndex != -1) + { + _temporarilyResetCurrentCell = true; + newCurrentCellCoordinates = new DataGridCellCoordinates(columnIndexInserted <= CurrentColumnIndex ? CurrentColumnIndex + 1 : CurrentColumnIndex, + CurrentSlot); + ResetCurrentCellCore(); + } + else + { + newCurrentCellCoordinates = new DataGridCellCoordinates(-1, -1); + } + return newCurrentCellCoordinates; + } + + internal void NotifyRemovedColumnPostNotification(DataGridCellCoordinates newCurrentCellCoordinates) + { + // Update current cell if needed + if (newCurrentCellCoordinates.ColumnIndex != -1) + { + Debug.Assert(CurrentColumnIndex == -1); + SetAndSelectCurrentCell(newCurrentCellCoordinates.ColumnIndex, newCurrentCellCoordinates.Slot, false /*forceCurrentCellSelection*/); + } + } + + internal void NotifyRemovedColumnPreNotification(DataGridColumn removedColumn) + { + Debug.Assert(removedColumn.Index >= 0); + Debug.Assert(removedColumn.OwningGrid == null); + + // Intentionally keep the DisplayIndex intact after detaching the column. + CorrectColumnIndexesAfterDeletion(removedColumn); + + CorrectColumnDisplayIndexesAfterDeletion(removedColumn); + + // If the detached column was frozen, a new column needs to take its place + if (removedColumn.IsFrozen) + { + removedColumn.IsFrozen = false; + CorrectColumnFrozenStates(); + } + + UpdateDisplayedColumns(); + + // Fix the existing rows by removing cells at correct index + int newColumnCount = ColumnsItemsInternal.Count; + + if (_rowsPresenter != null) + { + foreach (DataGridRow row in GetAllRows()) + { + if (row.Cells.Count > newColumnCount) + { + row.Cells.RemoveAt(removedColumn.Index); + } + } + _rowsPresenter.InvalidateArrange(); + } + + RemoveDisplayedColumnHeader(removedColumn); + } + + internal DataGridCellCoordinates OnRemovingColumn(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null); + Debug.Assert(dataGridColumn.Index >= 0 && dataGridColumn.Index < ColumnsItemsInternal.Count); + + DataGridCellCoordinates newCurrentCellCoordinates; + + _temporarilyResetCurrentCell = false; + int columnIndex = dataGridColumn.Index; + + // Reset the current cell's address if there is one. + if (CurrentColumnIndex != -1) + { + int newCurrentColumnIndex = CurrentColumnIndex; + if (columnIndex == newCurrentColumnIndex) + { + DataGridColumn? dataGridColumnNext = ColumnsInternal.GetNextVisibleColumn(ColumnsItemsInternal[columnIndex]); + if (dataGridColumnNext != null) + { + if (dataGridColumnNext.Index > columnIndex) + { + newCurrentColumnIndex = dataGridColumnNext.Index - 1; + } + else + { + newCurrentColumnIndex = dataGridColumnNext.Index; + } + } + else + { + DataGridColumn? dataGridColumnPrevious = ColumnsInternal.GetPreviousVisibleNonFillerColumn(ColumnsItemsInternal[columnIndex]); + if (dataGridColumnPrevious != null) + { + if (dataGridColumnPrevious.Index > columnIndex) + { + newCurrentColumnIndex = dataGridColumnPrevious.Index - 1; + } + else + { + newCurrentColumnIndex = dataGridColumnPrevious.Index; + } + } + else + { + newCurrentColumnIndex = -1; + } + } + } + else if (columnIndex < newCurrentColumnIndex) + { + newCurrentColumnIndex--; + } + newCurrentCellCoordinates = new DataGridCellCoordinates(newCurrentColumnIndex, (newCurrentColumnIndex == -1) ? -1 : CurrentSlot); + if (columnIndex == CurrentColumnIndex) + { + // If the commit fails, force a cancel edit + if (!CommitEdit(DataGridEditingUnit.Row, false /*exitEditingMode*/)) + { + CancelEdit(DataGridEditingUnit.Row, false /*raiseEvents*/); + } + bool success = SetCurrentCellCore(-1, -1); + Debug.Assert(success); + } + else + { + // Underlying data of deleted column is gone. It cannot be accessed anymore. + // Do not end editing mode so that CellValidation doesn't get raised, since that event needs the current formatted value. + _temporarilyResetCurrentCell = true; + bool success = SetCurrentCellCore(-1, -1); + Debug.Assert(success); + } + } + else + { + newCurrentCellCoordinates = new DataGridCellCoordinates(-1, -1); + } + + // If the last column is removed, delete all the rows first. + if (ColumnsItemsInternal.Count == 1) + { + ClearRows(false); + } + + // Is deleted column scrolled off screen? + if (dataGridColumn.IsVisible && + !dataGridColumn.IsFrozen && + DisplayData.FirstDisplayedScrollingCol >= 0) + { + // Deleted column is part of scrolling columns. + if (DisplayData.FirstDisplayedScrollingCol == dataGridColumn.Index) + { + // Deleted column is first scrolling column + _horizontalOffset -= _negHorizontalOffset; + _negHorizontalOffset = 0; + } + else if (!ColumnsInternal.DisplayInOrder(DisplayData.FirstDisplayedScrollingCol, dataGridColumn.Index)) + { + // Deleted column is displayed before first scrolling column + Debug.Assert(_horizontalOffset >= GetEdgedColumnWidth(dataGridColumn)); + _horizontalOffset -= GetEdgedColumnWidth(dataGridColumn); + } + + if (_hScrollBar != null && _hScrollBar.IsVisible) // + { + _hScrollBar.Value = _horizontalOffset; + } + } + + return newCurrentCellCoordinates; + } + + /// + /// Called when a column property changes, and its cells need to + /// adjust that that column change. + /// + internal void RefreshColumnElements(DataGridColumn dataGridColumn, string propertyName) + { + Debug.Assert(dataGridColumn != null); + + // Take care of the non-displayed loaded rows + for (int index = 0; index < _loadedRows.Count;) + { + DataGridRow dataGridRow = _loadedRows[index]; + Debug.Assert(dataGridRow != null); + if (!IsSlotVisible(dataGridRow.Slot)) + { + RefreshCellElement(dataGridColumn, dataGridRow, propertyName); + } + index++; + } + + // Take care of the displayed rows + if (_rowsPresenter != null) + { + foreach (DataGridRow row in GetAllRows()) + { + RefreshCellElement(dataGridColumn, row, propertyName); + } + // This update could change layout so we need to update our estimate and invalidate + InvalidateRowHeightEstimate(); + InvalidateMeasure(); + } + } + + /// + /// Adjusts the widths of all star columns with DisplayIndex >= displayIndex such that the total + /// width is adjusted by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. + /// + /// Starting column DisplayIndex. + /// Adjustment amount (positive for increase, negative for decrease). + /// Whether or not this adjustment was initiated by a user action. + /// The remaining amount of adjustment. + private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated) + { + double remainingAdjustment = adjustment; + if (MathUtilities.IsZero(remainingAdjustment)) + { + return remainingAdjustment; + } + bool increase = remainingAdjustment > 0; + + // Make an initial pass through the star columns to total up some values. + bool scaleStarWeights = false; + double totalStarColumnsWidth = 0; + double totalStarColumnsWidthLimit = 0; + double totalStarWeights = 0; + List starColumns = new List(); + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(c => c.Width.IsStar && c.IsVisible && (c.ActualCanUserResize || !userInitiated))) + { + if (column.DisplayIndex < displayIndex) + { + scaleStarWeights = true; + continue; + } + starColumns.Add(column); + totalStarWeights += column.Width.Value; + totalStarColumnsWidth += column.Width.DisplayValue; + totalStarColumnsWidthLimit += increase ? column.ActualMaxWidth : column.ActualMinWidth; + } + + // Set the new desired widths according to how much all the star columns can be adjusted without any + // of them being limited by their minimum or maximum widths (as that would distort their ratios). + double adjustmentLimit = totalStarColumnsWidthLimit - totalStarColumnsWidth; + adjustmentLimit = increase ? Math.Min(adjustmentLimit, adjustment) : Math.Max(adjustmentLimit, adjustment); + foreach (DataGridColumn starColumn in starColumns) + { + starColumn.SetWidthDesiredValue((totalStarColumnsWidth + adjustmentLimit) * starColumn.Width.Value / totalStarWeights); + } + + // Adjust the star column widths first towards their desired values, and then towards their limits. + remainingAdjustment = AdjustStarColumnWidths(displayIndex, remainingAdjustment, userInitiated, c => c.Width.DesiredValue); + remainingAdjustment = AdjustStarColumnWidths(displayIndex, remainingAdjustment, userInitiated, c => increase ? c.ActualMaxWidth : c.ActualMinWidth); + + // Set the new star value weights according to how much the total column widths have changed. + // Only do this if there were other star columns to the left, though. If there weren't any then that means + // all the star columns were adjusted at the same time, and therefore, their ratios have not changed. + if (scaleStarWeights) + { + double starRatio = (totalStarColumnsWidth + adjustment - remainingAdjustment) / totalStarColumnsWidth; + foreach (DataGridColumn starColumn in starColumns) + { + starColumn.SetWidthStarValue(Math.Min(double.MaxValue, starRatio * starColumn.Width.Value)); + } + } + + return remainingAdjustment; + } + + /// + /// Adjusts the widths of all star columns with DisplayIndex >= displayIndex such that the total + /// width is adjusted by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. The columns will stop adjusting + /// once they hit their target widths. + /// + /// Starting column DisplayIndex. + /// Adjustment amount (positive for increase, negative for decrease). + /// Whether or not this adjustment was initiated by a user action. + /// The target width of the column. + /// The remaining amount of adjustment. + private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func targetWidth) + { + if (MathUtilities.IsZero(remainingAdjustment)) + { + return remainingAdjustment; + } + bool increase = remainingAdjustment > 0; + + double totalStarWeights = 0d; + double totalStarColumnsWidth = 0d; + + // Order the star columns according to which one will hit their target width (or min/max limit) first. + // Each KeyValuePair represents a column (as the key) and an ordering factor (as the value). The ordering factor + // is computed based on the distance from each column's current display width to its target width. Because each column + // could have different star ratios, though, this distance is then adjusted according to its star value. A column with + // a larger star value, for example, will change size more rapidly than a column with a lower star value. + List> starColumnPairs = new List>(); + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns( + c => c.Width.IsStar && c.DisplayIndex >= displayIndex && c.IsVisible && c.Width.Value > 0 && (c.ActualCanUserResize || !userInitiated))) + { + int insertIndex = 0; + double distanceToTarget = Math.Min(column.ActualMaxWidth, Math.Max(targetWidth(column), column.ActualMinWidth)) - column.Width.DisplayValue; + double factor = (increase ? Math.Max(0, distanceToTarget) : Math.Min(0, distanceToTarget)) / column.Width.Value; + foreach (KeyValuePair starColumnPair in starColumnPairs) + { + if (increase ? factor <= starColumnPair.Value : factor >= starColumnPair.Value) + { + break; + } + insertIndex++; + } + starColumnPairs.Insert(insertIndex, new KeyValuePair(column, factor)); + totalStarWeights += column.Width.Value; + totalStarColumnsWidth += column.Width.DisplayValue; + } + + // Adjust the column widths one at a time until they either hit their individual target width + // or the total remaining amount to adjust has been depleted. + foreach (KeyValuePair starColumnPair in starColumnPairs) + { + double distanceToTarget = starColumnPair.Value * starColumnPair.Key.Width.Value; + double distanceAvailable = (starColumnPair.Key.Width.Value * remainingAdjustment) / totalStarWeights; + double adjustment = increase ? Math.Min(distanceToTarget, distanceAvailable) : Math.Max(distanceToTarget, distanceAvailable); + + remainingAdjustment -= adjustment; + totalStarWeights -= starColumnPair.Key.Width.Value; + starColumnPair.Key.SetWidthDisplayValue(Math.Max(MinimumStarColumnWidth, starColumnPair.Key.Width.DisplayValue + adjustment)); + } + + return remainingAdjustment; + } + + private bool ComputeDisplayedColumns() + { + bool invalidate = false; + int numVisibleScrollingCols = 0; + int visibleScrollingColumnsTmp = 0; + double displayWidth = CellsWidth; + double cx = 0; + int firstDisplayedFrozenCol = -1; + int firstDisplayedScrollingCol = DisplayData.FirstDisplayedScrollingCol; + + // the same problem with negative numbers: + // if the width passed in is negative, then return 0 + if (displayWidth <= 0 || ColumnsInternal.VisibleColumnCount == 0) + { + DisplayData.FirstDisplayedScrollingCol = -1; + DisplayData.LastTotallyDisplayedScrollingCol = -1; + return invalidate; + } + + foreach (DataGridColumn dataGridColumn in ColumnsInternal.GetVisibleFrozenColumns()) + { + if (firstDisplayedFrozenCol == -1) + { + firstDisplayedFrozenCol = dataGridColumn.Index; + } + cx += GetEdgedColumnWidth(dataGridColumn); + if (cx >= displayWidth) + { + break; + } + } + + Debug.Assert(cx <= ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth()); + + if (cx < displayWidth && firstDisplayedScrollingCol >= 0) + { + DataGridColumn? dataGridColumn = ColumnsItemsInternal[firstDisplayedScrollingCol]; + if (dataGridColumn.IsFrozen) + { + dataGridColumn = ColumnsInternal.FirstVisibleScrollingColumn; + _negHorizontalOffset = 0; + if (dataGridColumn == null) + { + DisplayData.FirstDisplayedScrollingCol = DisplayData.LastTotallyDisplayedScrollingCol = -1; + return invalidate; + } + else + { + firstDisplayedScrollingCol = dataGridColumn.Index; + } + } + + cx -= _negHorizontalOffset; + while (cx < displayWidth && dataGridColumn != null) + { + cx += GetEdgedColumnWidth(dataGridColumn); + visibleScrollingColumnsTmp++; + dataGridColumn = ColumnsInternal.GetNextVisibleColumn(dataGridColumn); + } + numVisibleScrollingCols = visibleScrollingColumnsTmp; + + // if we inflate the data area then we paint columns to the left of firstDisplayedScrollingCol + if (cx < displayWidth) + { + Debug.Assert(firstDisplayedScrollingCol >= 0); + //first minimize value of _negHorizontalOffset + if (_negHorizontalOffset > 0) + { + invalidate = true; + if (displayWidth - cx > _negHorizontalOffset) + { + cx += _negHorizontalOffset; + _horizontalOffset -= _negHorizontalOffset; + if (_horizontalOffset < LayoutHelper.LayoutEpsilon) + { + // Snap to zero to avoid trying to partially scroll in first scrolled off column below + _horizontalOffset = 0; + } + + _negHorizontalOffset = 0; + } + else + { + _horizontalOffset -= displayWidth - cx; + _negHorizontalOffset -= displayWidth - cx; + cx = displayWidth; + } + + // Make sure the HorizontalAdjustment is not greater than the new HorizontalOffset + // since it would cause an assertion failure in DataGridCellsPresenter.ShouldDisplayCell + // called by DataGridCellsPresenter.MeasureOverride. + HorizontalAdjustment = Math.Min(HorizontalAdjustment, _horizontalOffset); + } + // second try to scroll entire columns + if (cx < displayWidth && _horizontalOffset > 0) + { + Debug.Assert(_negHorizontalOffset == 0); + dataGridColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn(ColumnsItemsInternal[firstDisplayedScrollingCol]); + while (dataGridColumn != null && cx + GetEdgedColumnWidth(dataGridColumn) <= displayWidth) + { + cx += GetEdgedColumnWidth(dataGridColumn); + visibleScrollingColumnsTmp++; + invalidate = true; + firstDisplayedScrollingCol = dataGridColumn.Index; + _horizontalOffset -= GetEdgedColumnWidth(dataGridColumn); + dataGridColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn(dataGridColumn); + } + } + // third try to partially scroll in first scrolled off column + if (cx < displayWidth && _horizontalOffset > 0) + { + Debug.Assert(_negHorizontalOffset == 0); + dataGridColumn = ColumnsInternal.GetPreviousVisibleScrollingColumn(ColumnsItemsInternal[firstDisplayedScrollingCol]); + Debug.Assert(dataGridColumn != null); + Debug.Assert(GetEdgedColumnWidth(dataGridColumn) > displayWidth - cx); + firstDisplayedScrollingCol = dataGridColumn.Index; + _negHorizontalOffset = GetEdgedColumnWidth(dataGridColumn) - displayWidth + cx; + _horizontalOffset -= displayWidth - cx; + visibleScrollingColumnsTmp++; + invalidate = true; + cx = displayWidth; + Debug.Assert(_negHorizontalOffset == GetNegHorizontalOffsetFromHorizontalOffset(_horizontalOffset)); + } + + // update the number of visible columns to the new reality + Debug.Assert(numVisibleScrollingCols <= visibleScrollingColumnsTmp, "the number of displayed columns can only grow"); + numVisibleScrollingCols = visibleScrollingColumnsTmp; + } + + int jumpFromFirstVisibleScrollingCol = numVisibleScrollingCols - 1; + if (cx > displayWidth) + { + jumpFromFirstVisibleScrollingCol--; + } + + Debug.Assert(jumpFromFirstVisibleScrollingCol >= -1); + + if (jumpFromFirstVisibleScrollingCol < 0) + { + DisplayData.LastTotallyDisplayedScrollingCol = -1; // no totally visible scrolling column at all + } + else + { + Debug.Assert(firstDisplayedScrollingCol >= 0); + dataGridColumn = ColumnsItemsInternal[firstDisplayedScrollingCol]; + for (int jump = 0; jump < jumpFromFirstVisibleScrollingCol; jump++) + { + dataGridColumn = ColumnsInternal.GetNextVisibleColumn(dataGridColumn); + Debug.Assert(dataGridColumn != null); + } + DisplayData.LastTotallyDisplayedScrollingCol = dataGridColumn.Index; + } + } + else + { + DisplayData.LastTotallyDisplayedScrollingCol = -1; + } + DisplayData.FirstDisplayedScrollingCol = firstDisplayedScrollingCol; + + return invalidate; + } + + private int ComputeFirstVisibleScrollingColumn() + { + if (ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth() >= CellsWidth) + { + // Not enough room for scrolling columns. + _negHorizontalOffset = 0; + return -1; + } + + DataGridColumn? dataGridColumn = ColumnsInternal.FirstVisibleScrollingColumn; + + if (_horizontalOffset == 0) + { + _negHorizontalOffset = 0; + return (dataGridColumn == null) ? -1 : dataGridColumn.Index; + } + + double cx = 0; + while (dataGridColumn != null) + { + cx += GetEdgedColumnWidth(dataGridColumn); + if (cx > _horizontalOffset) + { + break; + } + dataGridColumn = ColumnsInternal.GetNextVisibleColumn(dataGridColumn); + } + + if (dataGridColumn == null) + { + Debug.Assert(cx <= _horizontalOffset); + dataGridColumn = ColumnsInternal.FirstVisibleScrollingColumn; + if (dataGridColumn == null) + { + _negHorizontalOffset = 0; + return -1; + } + if (!MathUtils.AreClose(_negHorizontalOffset, _horizontalOffset)) + { + _negHorizontalOffset = 0; + } + return dataGridColumn.Index; + } + _negHorizontalOffset = GetEdgedColumnWidth(dataGridColumn) - (cx - _horizontalOffset); + return dataGridColumn.Index; + } + + private void CorrectColumnDisplayIndexesAfterDeletion(DataGridColumn deletedColumn) + { + // Column indexes have already been adjusted. + // This column has already been detached and has retained its old Index and DisplayIndex + + Debug.Assert(deletedColumn != null); + Debug.Assert(deletedColumn.OwningGrid == null); + Debug.Assert(deletedColumn.Index >= 0); + Debug.Assert(deletedColumn.DisplayIndexWithFiller >= 0); + + try + { + InDisplayIndexAdjustments = true; + + // The DisplayIndex of columns greater than the deleted column need to be decremented, + // as do the DisplayIndexMap values of modified column Indexes + DataGridColumn? column; + ColumnsInternal.DisplayIndexMap.RemoveAt(deletedColumn.DisplayIndexWithFiller); + for (int displayIndex = 0; displayIndex < ColumnsInternal.DisplayIndexMap.Count; displayIndex++) + { + if (ColumnsInternal.DisplayIndexMap[displayIndex] > deletedColumn.Index) + { + ColumnsInternal.DisplayIndexMap[displayIndex]--; + } + if (displayIndex >= deletedColumn.DisplayIndexWithFiller) + { + column = ColumnsInternal.GetColumnAtDisplayIndex(displayIndex); + Debug.Assert(column != null); + column.DisplayIndexWithFiller -= 1; + column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on + } + } + + // Now raise all the OnColumnDisplayIndexChanged events + FlushDisplayIndexChanged(true /*raiseEvent*/); + } + finally + { + InDisplayIndexAdjustments = false; + FlushDisplayIndexChanged(false /*raiseEvent*/); + } + } + + private void CorrectColumnDisplayIndexesAfterInsertion(DataGridColumn insertedColumn) + { + Debug.Assert(insertedColumn != null); + Debug.Assert(insertedColumn.OwningGrid == this); + if (insertedColumn.DisplayIndexWithFiller == -1 || insertedColumn.DisplayIndexWithFiller >= ColumnsItemsInternal.Count) + { + // Developer did not assign a DisplayIndex or picked a large number. + // Choose the Index as the DisplayIndex. + insertedColumn.DisplayIndexWithFiller = insertedColumn.Index; + } + + try + { + InDisplayIndexAdjustments = true; + + // The DisplayIndex of columns greater than the inserted column need to be incremented, + // as do the DisplayIndexMap values of modified column Indexes + DataGridColumn? column; + for (int displayIndex = 0; displayIndex < ColumnsInternal.DisplayIndexMap.Count; displayIndex++) + { + if (ColumnsInternal.DisplayIndexMap[displayIndex] >= insertedColumn.Index) + { + ColumnsInternal.DisplayIndexMap[displayIndex]++; + } + if (displayIndex >= insertedColumn.DisplayIndexWithFiller) + { + column = ColumnsInternal.GetColumnAtDisplayIndex(displayIndex); + Debug.Assert(column != null); + column.DisplayIndexWithFiller++; + column.DisplayIndexHasChanged = true; // OnColumnDisplayIndexChanged needs to be raised later on + } + } + ColumnsInternal.DisplayIndexMap.Insert(insertedColumn.DisplayIndexWithFiller, insertedColumn.Index); + + // Now raise all the OnColumnDisplayIndexChanged events + FlushDisplayIndexChanged(true /*raiseEvent*/); + } + finally + { + InDisplayIndexAdjustments = false; + FlushDisplayIndexChanged(false /*raiseEvent*/); + } + } + + private void CorrectColumnFrozenStates() + { + int index = 0; + double frozenColumnWidth = 0; + double oldFrozenColumnWidth = 0; + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns()) + { + if (column.IsFrozen) + { + oldFrozenColumnWidth += column.ActualWidth; + } + column.IsFrozen = index < FrozenColumnCountWithFiller; + if (column.IsFrozen) + { + frozenColumnWidth += column.ActualWidth; + } + index++; + } + if (HorizontalOffset > Math.Max(0, frozenColumnWidth - oldFrozenColumnWidth)) + { + UpdateHorizontalOffset(HorizontalOffset - frozenColumnWidth + oldFrozenColumnWidth); + } + else + { + UpdateHorizontalOffset(0); + } + } + + private void CorrectColumnIndexesAfterDeletion(DataGridColumn deletedColumn) + { + Debug.Assert(deletedColumn != null); + for (int columnIndex = deletedColumn.Index; columnIndex < ColumnsItemsInternal.Count; columnIndex++) + { + ColumnsItemsInternal[columnIndex].Index -= 1; + Debug.Assert(ColumnsItemsInternal[columnIndex].Index == columnIndex); + } + } + + private void CorrectColumnIndexesAfterInsertion(DataGridColumn insertedColumn, int insertionCount) + { + Debug.Assert(insertedColumn != null); + Debug.Assert(insertionCount > 0); + for (int columnIndex = insertedColumn.Index + insertionCount; columnIndex < ColumnsItemsInternal.Count; columnIndex++) + { + ColumnsItemsInternal[columnIndex].Index = columnIndex; + } + } + + /// + /// Decreases the width of a non-star column by the given amount, if possible. If the total desired + /// adjustment amount could not be met, the remaining amount of adjustment is returned. The adjustment + /// stops when the column's target width has been met. + /// + /// Column to adjust. + /// The target width of the column (in pixels). + /// Amount to decrease (in pixels). + /// The remaining amount of adjustment. + private static double DecreaseNonStarColumnWidth(DataGridColumn column, double targetWidth, double amount) + { + Debug.Assert(amount < 0); + Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star); + + if (MathUtilities.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) + { + return amount; + } + + double adjustment = Math.Max( + column.ActualMinWidth - column.Width.DisplayValue, + Math.Max(targetWidth - column.Width.DisplayValue, amount)); + + column.SetWidthDisplayValue(column.Width.DisplayValue + adjustment); + return amount - adjustment; + } + + /// + /// Decreases the widths of all non-star columns with DisplayIndex >= displayIndex such that the total + /// width is decreased by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. The adjustment stops when + /// the column's target width has been met. + /// + /// Starting column DisplayIndex. + /// The target width of the column (in pixels). + /// Amount to decrease (in pixels). + /// Whether or not to reverse the order in which the columns are traversed. + /// Whether or not to adjust widths of columns that do not yet have their initial desired width. + /// The remaining amount of adjustment. + private double DecreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) + { + if (MathUtilities.GreaterThanOrClose(amount, 0)) + { + return amount; + } + + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(reverse, + column => + column.IsVisible && + column.Width.UnitType != DataGridLengthUnitType.Star && + column.DisplayIndex >= displayIndex && + column.ActualCanUserResize && + (affectNewColumns || column.IsInitialDesiredWidthDetermined))) + { + amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount); + if (MathUtilities.IsZero(amount)) + { + break; + } + } + return amount; + } + + private void FlushDisplayIndexChanged(bool raiseEvent) + { + foreach (DataGridColumn column in ColumnsItemsInternal) + { + if (column.DisplayIndexHasChanged) + { + column.DisplayIndexHasChanged = false; + if (raiseEvent) + { + Debug.Assert(column != ColumnsInternal.RowGroupSpacerColumn); + NotifyColumnDisplayIndexChanged(column); + } + } + } + } + + private bool GetColumnEffectiveReadOnlyState(DataGridColumn? dataGridColumn) + { + Debug.Assert(dataGridColumn != null); + + return IsReadOnly || dataGridColumn.IsReadOnly || dataGridColumn is DataGridFillerColumn; + } + + /// + /// Returns the absolute coordinate of the left edge of the given column (including + /// the potential gridline - that is the left edge of the gridline is returned). Note that + /// the column does not need to be in the display area. + /// + private double GetColumnXFromIndex(int index) + { + Debug.Assert(index < ColumnsItemsInternal.Count); + Debug.Assert(ColumnsItemsInternal[index].IsVisible); + + double x = 0; + foreach (DataGridColumn column in ColumnsInternal.GetVisibleColumns()) + { + if (index == column.Index) + { + break; + } + x += GetEdgedColumnWidth(column); + } + return x; + } + + private double GetNegHorizontalOffsetFromHorizontalOffset(double horizontalOffset) + { + foreach (DataGridColumn column in ColumnsInternal.GetVisibleScrollingColumns()) + { + if (GetEdgedColumnWidth(column) > horizontalOffset) + { + break; + } + horizontalOffset -= GetEdgedColumnWidth(column); + } + return horizontalOffset; + } + + /// + /// Increases the width of a non-star column by the given amount, if possible. If the total desired + /// adjustment amount could not be met, the remaining amount of adjustment is returned. The adjustment + /// stops when the column's target width has been met. + /// + /// Column to adjust. + /// The target width of the column (in pixels). + /// Amount to increase (in pixels). + /// The remaining amount of adjustment. + private static double IncreaseNonStarColumnWidth(DataGridColumn column, double targetWidth, double amount) + { + Debug.Assert(amount > 0); + Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star); + + if (targetWidth <= column.Width.DisplayValue) + { + return amount; + } + + double adjustment = Math.Min( + column.ActualMaxWidth - column.Width.DisplayValue, + Math.Min(targetWidth - column.Width.DisplayValue, amount)); + + column.SetWidthDisplayValue(column.Width.DisplayValue + adjustment); + return amount - adjustment; + } + + /// + /// Increases the widths of all non-star columns with DisplayIndex >= displayIndex such that the total + /// width is increased by the given amount, if possible. If the total desired adjustment amount + /// could not be met, the remaining amount of adjustment is returned. The adjustment stops when + /// the column's target width has been met. + /// + /// Starting column DisplayIndex. + /// The target width of the column (in pixels). + /// Amount to increase (in pixels). + /// Whether or not to reverse the order in which the columns are traversed. + /// Whether or not to adjust widths of columns that do not yet have their initial desired width. + /// The remaining amount of adjustment. + private double IncreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) + { + if (MathUtilities.LessThanOrClose(amount, 0)) + { + return amount; + } + + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(reverse, + column => + column.IsVisible && + column.Width.UnitType != DataGridLengthUnitType.Star && + column.DisplayIndex >= displayIndex && + column.ActualCanUserResize && + (affectNewColumns || column.IsInitialDesiredWidthDetermined))) + { + amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount); + if (MathUtilities.IsZero(amount)) + { + break; + } + } + return amount; + } + + private void InsertDisplayedColumnHeader(DataGridColumn dataGridColumn) + { + Debug.Assert(dataGridColumn != null); + if (_columnHeadersPresenter != null) + { + dataGridColumn.HeaderCell.IsVisible = dataGridColumn.IsVisible; + Debug.Assert(!_columnHeadersPresenter.Children.Contains(dataGridColumn.HeaderCell)); + _columnHeadersPresenter.Children.Insert(dataGridColumn.DisplayIndexWithFiller, dataGridColumn.HeaderCell); + } + } + + private static void RefreshCellElement(DataGridColumn dataGridColumn, DataGridRow dataGridRow, string propertyName) + { + Debug.Assert(dataGridColumn != null); + Debug.Assert(dataGridRow != null); + + DataGridCell dataGridCell = dataGridRow.Cells[dataGridColumn.Index]; + Debug.Assert(dataGridCell != null); + if (dataGridCell.Content is Control element) + { + dataGridColumn.RefreshCellContent(element, propertyName); + } + } + + private void RemoveAutoGeneratedColumns() + { + int index = 0; + _autoGeneratingColumnOperationCount++; + try + { + while (index < ColumnsInternal.Count) + { + // Skip over the user columns + while (index < ColumnsInternal.Count && !ColumnsInternal[index].IsAutoGenerated) + { + index++; + } + // Remove the autogenerated columns + while (index < ColumnsInternal.Count && ColumnsInternal[index].IsAutoGenerated) + { + ColumnsInternal.RemoveAt(index); + } + } + ColumnsInternal.AutogeneratedColumnCount = 0; + } + finally + { + _autoGeneratingColumnOperationCount--; + } + } + + private bool ScrollColumnIntoView(int columnIndex) + { + Debug.Assert(columnIndex >= 0 && columnIndex < ColumnsItemsInternal.Count); + + if (DisplayData.FirstDisplayedScrollingCol != -1 && + !ColumnsItemsInternal[columnIndex].IsFrozen && + (columnIndex != DisplayData.FirstDisplayedScrollingCol || _negHorizontalOffset > 0)) + { + int columnsToScroll; + if (ColumnsInternal.DisplayInOrder(columnIndex, DisplayData.FirstDisplayedScrollingCol)) + { + columnsToScroll = ColumnsInternal.GetColumnCount(true /* isVisible */, false /* isFrozen */, columnIndex, DisplayData.FirstDisplayedScrollingCol); + if (_negHorizontalOffset > 0) + { + columnsToScroll++; + } + ScrollColumns(-columnsToScroll); + } + else if (columnIndex == DisplayData.FirstDisplayedScrollingCol && _negHorizontalOffset > 0) + { + ScrollColumns(-1); + } + else if (DisplayData.LastTotallyDisplayedScrollingCol == -1 || + (DisplayData.LastTotallyDisplayedScrollingCol != columnIndex && + ColumnsInternal.DisplayInOrder(DisplayData.LastTotallyDisplayedScrollingCol, columnIndex))) + { + double xColumnLeftEdge = GetColumnXFromIndex(columnIndex); + double xColumnRightEdge = xColumnLeftEdge + GetEdgedColumnWidth(ColumnsItemsInternal[columnIndex]); + double change = xColumnRightEdge - HorizontalOffset - CellsWidth; + double widthRemaining = change; + + DataGridColumn newFirstDisplayedScrollingCol = ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]; + DataGridColumn? nextColumn = ColumnsInternal.GetNextVisibleColumn(newFirstDisplayedScrollingCol); + double newColumnWidth = GetEdgedColumnWidth(newFirstDisplayedScrollingCol) - _negHorizontalOffset; + while (nextColumn != null && widthRemaining >= newColumnWidth) + { + widthRemaining -= newColumnWidth; + newFirstDisplayedScrollingCol = nextColumn; + newColumnWidth = GetEdgedColumnWidth(newFirstDisplayedScrollingCol); + nextColumn = ColumnsInternal.GetNextVisibleColumn(newFirstDisplayedScrollingCol); + _negHorizontalOffset = 0; + } + _negHorizontalOffset += widthRemaining; + DisplayData.LastTotallyDisplayedScrollingCol = columnIndex; + if (newFirstDisplayedScrollingCol.Index == columnIndex) + { + _negHorizontalOffset = 0; + double frozenColumnWidth = ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth(); + // If the entire column cannot be displayed, we want to start showing it from its LeftEdge + if (newColumnWidth > (CellsWidth - frozenColumnWidth)) + { + DisplayData.LastTotallyDisplayedScrollingCol = -1; + change = xColumnLeftEdge - HorizontalOffset - frozenColumnWidth; + } + } + DisplayData.FirstDisplayedScrollingCol = newFirstDisplayedScrollingCol.Index; + + // At this point DisplayData.FirstDisplayedScrollingColumn and LastDisplayedScrollingColumn + // should be correct + if (change != 0) + { + UpdateHorizontalOffset(HorizontalOffset + change); + } + } + } + return true; + } + + private void ScrollColumns(int columns) + { + DataGridColumn? newFirstVisibleScrollingCol = null; + DataGridColumn? dataGridColumnTmp; + int colCount = 0; + if (columns > 0) + { + if (DisplayData.LastTotallyDisplayedScrollingCol >= 0) + { + dataGridColumnTmp = ColumnsItemsInternal[DisplayData.LastTotallyDisplayedScrollingCol]; + while (colCount < columns && dataGridColumnTmp != null) + { + dataGridColumnTmp = ColumnsInternal.GetNextVisibleColumn(dataGridColumnTmp); + colCount++; + } + + if (dataGridColumnTmp == null) + { + // no more column to display on the right of the last totally seen column + return; + } + } + Debug.Assert(DisplayData.FirstDisplayedScrollingCol >= 0); + dataGridColumnTmp = ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]; + colCount = 0; + while (colCount < columns && dataGridColumnTmp != null) + { + dataGridColumnTmp = ColumnsInternal.GetNextVisibleColumn(dataGridColumnTmp); + colCount++; + } + newFirstVisibleScrollingCol = dataGridColumnTmp; + } + + if (columns < 0) + { + Debug.Assert(DisplayData.FirstDisplayedScrollingCol >= 0); + dataGridColumnTmp = ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]; + if (_negHorizontalOffset > 0) + { + colCount++; + } + while (colCount < -columns && dataGridColumnTmp != null) + { + dataGridColumnTmp = ColumnsInternal.GetPreviousVisibleScrollingColumn(dataGridColumnTmp); + colCount++; + } + newFirstVisibleScrollingCol = dataGridColumnTmp; + if (newFirstVisibleScrollingCol == null) + { + if (_negHorizontalOffset == 0) + { + // no more column to display on the left of the first seen column + return; + } + newFirstVisibleScrollingCol = ColumnsItemsInternal[DisplayData.FirstDisplayedScrollingCol]; + } + } + + double newColOffset = 0; + foreach (DataGridColumn dataGridColumn in ColumnsInternal.GetVisibleScrollingColumns()) + { + if (dataGridColumn == newFirstVisibleScrollingCol) + { + break; + } + newColOffset += GetEdgedColumnWidth(dataGridColumn); + } + + UpdateHorizontalOffset(newColOffset); + } + + private void UpdateDisplayedColumns() + { + DisplayData.FirstDisplayedScrollingCol = ComputeFirstVisibleScrollingColumn(); + ComputeDisplayedColumns(); + } + + private static DataGridBoundColumn GetDataGridColumnFromType(Type type) + { + Debug.Assert(type != null); + if (type == typeof(bool)) + { + return new DataGridCheckBoxColumn(); + } + if (type == typeof(bool?)) + { + return new DataGridCheckBoxColumn + { + IsThreeState = true + }; + } + return new DataGridTextColumn(); + } + + private void AutoGenerateColumnsPrivate() + { + if (!_measured || (_autoGeneratingColumnOperationCount > 0)) + { + // Reading the DataType when we generate columns could cause the CollectionView to + // raise a Reset if its Enumeration changed. In that case, we don't want to generate again. + return; + } + + _autoGeneratingColumnOperationCount++; + try + { + // Always remove existing autogenerated columns before generating new ones + RemoveAutoGeneratedColumns(); + GenerateColumnsFromProperties(); + EnsureRowsPresenterVisibility(); + InvalidateRowHeightEstimate(); + } + finally + { + _autoGeneratingColumnOperationCount--; + } + } + + private void GenerateColumnsFromProperties() + { + // Autogenerated Columns are added at the end so the user columns appear first + if (DataConnection.DataProperties.Length > 0) + { + List> columnOrderPairs = new List>(); + + // Generate the columns + foreach (PropertyInfo propertyInfo in DataConnection.DataProperties) + { + string columnHeader = propertyInfo.Name; + int columnOrder = DefaultColumnDisplayOrder; + + // Check if DisplayAttribute is defined on the property + object[] attributes = propertyInfo.GetCustomAttributes(typeof(DisplayAttribute), true); + if (attributes.Length > 0) + { + DisplayAttribute? displayAttribute = attributes[0] as DisplayAttribute; + Debug.Assert(displayAttribute != null); + + bool? autoGenerateField = displayAttribute.GetAutoGenerateField(); + if (autoGenerateField.HasValue && autoGenerateField.Value == false) + { + // Abort column generation because we aren't supposed to auto-generate this field + continue; + } + + string? header = displayAttribute.GetShortName(); + if (header != null) + { + columnHeader = header; + } + + int? order = displayAttribute.GetOrder(); + if (order.HasValue) + { + columnOrder = order.Value; + } + } + + // Generate a single column and determine its relative order + int insertIndex = 0; + if (columnOrder == int.MaxValue) + { + insertIndex = columnOrderPairs.Count; + } + else + { + foreach (KeyValuePair columnOrderPair in columnOrderPairs) + { + if (columnOrderPair.Key > columnOrder) + { + break; + } + insertIndex++; + } + } + DataGridAutoGeneratingColumnEventArgs columnArgs = GenerateColumn(propertyInfo.PropertyType, propertyInfo.Name, columnHeader); + columnOrderPairs.Insert(insertIndex, new KeyValuePair(columnOrder, columnArgs)); + } + + // Add the columns to the DataGrid in the correct order + foreach (KeyValuePair columnOrderPair in columnOrderPairs) + { + AddGeneratedColumn(columnOrderPair.Value); + } + } + else if (DataConnection.DataIsPrimitive) + { + Debug.Assert(DataConnection.DataType != null); + AddGeneratedColumn(GenerateColumn(DataConnection.DataType, string.Empty, DataConnection.DataType.Name)); + } + } + + private static DataGridAutoGeneratingColumnEventArgs GenerateColumn(Type propertyType, string propertyName, string header) + { + // Create a new DataBoundColumn for the Property + DataGridBoundColumn newColumn = GetDataGridColumnFromType(propertyType); + newColumn.Binding = new Binding(propertyName); + newColumn.Header = header; + newColumn.IsAutoGenerated = true; + return new DataGridAutoGeneratingColumnEventArgs(propertyName, propertyType, newColumn); + } + + private bool AddGeneratedColumn(DataGridAutoGeneratingColumnEventArgs e) + { + // Raise the AutoGeneratingColumn event in case the user wants to Cancel or Replace the + // column being generated + OnAutoGeneratingColumn(e); + if (e.Cancel) + { + return false; + } + if (e.Column != null) + { + // Set the IsAutoGenerated flag here in case the user provides a custom autogenerated column + e.Column.IsAutoGenerated = true; + // TODO 需要审查 + ColumnsInternal.Add(e.Column); + ColumnsInternal.AutogeneratedColumnCount++; + } + + return true; + } + + private void ProcessFrozenColumnCount() + { + CorrectColumnFrozenStates(); + ComputeScrollBarsLayout(); + + InvalidateColumnHeadersArrange(); + InvalidateCellsArrange(); + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls.DataGrid/DataGrid.Privates.cs b/src/AtomUI.Controls.DataGrid/DataGrid.Privates.cs index d97a9d150..31bd4c6a9 100644 --- a/src/AtomUI.Controls.DataGrid/DataGrid.Privates.cs +++ b/src/AtomUI.Controls.DataGrid/DataGrid.Privates.cs @@ -2,11 +2,14 @@ using System.Collections.Specialized; using System.ComponentModel.DataAnnotations; using System.Diagnostics; using AtomUI.Controls.Data; +using AtomUI.Controls.Utils; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace AtomUI.Controls; @@ -30,11 +33,6 @@ public partial class DataGrid private set; } - internal DataGridColumnCollection ColumnsInternal - { - get; - } - internal double RowHeightEstimate { get; @@ -75,18 +73,67 @@ public partial class DataGrid } } - private DataGridCellCoordinates CurrentCellCoordinates + private DataGridCellCoordinates CurrentCellCoordinates { get; set; } + + internal double RowGroupHeaderHeightEstimate { get; private set; } + + internal bool ContainsFocus { get; private set; } + + internal double NegVerticalOffset { get; private set; } + + private int NoSelectionChangeCount { - get; - set; + get => _noSelectionChangeCount; + set + { + _noSelectionChangeCount = value; + if (value == 0) + { + FlushSelectionChanged(); + } + } } - internal double RowGroupHeaderHeightEstimate - { - 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; } + /// + /// 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. + /// + internal bool UsesStarSizing => ColumnsInternal.VisibleStarColumnCount > 0 && + (!RowsPresenterAvailableSize.HasValue || !double.IsPositiveInfinity(RowsPresenterAvailableSize.Value.Width)); + + /// + /// 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. + /// + internal bool AutoSizingColumns + { + get => _autoSizingColumns; + set + { + if (_autoSizingColumns && !value) + { + 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 ScrollBar? VerticalScrollBar => _vScrollBar; + #endregion /// @@ -111,6 +158,21 @@ public partial class DataGrid private const double DefaultMinColumnWidth = 20; private const double DefaultMaxColumnWidth = double.PositiveInfinity; + private List _bindingValidationErrors = []; + private IDisposable? _validationSubscription; + + private INotifyCollectionChanged? _topLevelGroup; + private ContentControl? _clipboardContentControl; + + private Visual? _bottomRightCorner; + private DataGridRowsPresenter? _rowsPresenter; + private ScrollBar? _vScrollBar; + private ScrollBar? _hScrollBar; + + private ContentControl? _topLeftCornerHeader; + private ContentControl? _topRightCornerHeader; + private Control? _frozenColumnScrollBarSpacer; + // Nth row of rows 0..N that make up the RowHeightEstimate private int _lastEstimatedRow; private List _loadedRows; @@ -119,9 +181,21 @@ public partial class DataGrid private Queue _lostFocusActions; private IndexToValueTable _showDetailsTable; private DataGridSelectedItemsCollection _selectedItems; - private List _bindingValidationErrors; private double _rowHeaderDesiredWidth; private int? _mouseOverRowIndex; // -1 is used for the 'new row' + private bool _makeFirstDisplayedCellCurrentCellPending; + private bool _measured; + private int _noSelectionChangeCount; + private bool _scrollingByHeight; + private bool _temporarilyResetCurrentCell; + private DataGridColumn? _previousCurrentColumn; + private object? _previousCurrentItem; + private double[] _rowGroupHeightsByLevel; + private object? _uneditedValue; // Represents the original current cell value at the time it enters editing mode. + + // 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; @@ -129,49 +203,64 @@ public partial class DataGrid private bool _areHandlersSuspended; private bool _autoSizingColumns; private IndexToValueTable _collapsedSlotsTable; - // private Control _clickedElement; + 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; + private bool _successfullyUpdatedSelection; + + // 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; 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; + 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)) + { + var rowGroupInfo = RowGroupHeadersTable.GetValueAt(RowGroupHeadersTable.GetPreviousIndex(CurrentSlot)); + Debug.Assert(rowGroupInfo != null); + ExpandRowGroupParentChain(rowGroupInfo.Level, rowGroupInfo.Slot); + } + + if (CurrentColumn != _previousCurrentColumn + || CurrentItem != _previousCurrentItem) + { + CoerceSelectedItem(); + _previousCurrentColumn = CurrentColumn; + _previousCurrentItem = CurrentItem; + + OnCurrentCellChanged(EventArgs.Empty); + } + + _flushCurrentCellChanged = false; } #region 事件处理器 @@ -186,121 +275,121 @@ public partial class DataGrid private void HandleKeyUp(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); - // } - // } + 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); + } + } } private void HandleGotFocus(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(); - // } + 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(); + } } private void HandleLostFocus(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; - // } - // } - // } + _focusedObject = null; + if (ContainsFocus) + { + bool focusLeftDataGrid = true; + bool dataGridWillReceiveRoutedEvent = true; + Visual? focusedObject = FocusUtils.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 += HandleExternalEditingElementLostFocus; + } + } + } } private void HandleColumnsInternalCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -330,11 +419,6 @@ public partial class DataGrid // PseudoClasses.Set(":empty-rows", !DataConnection.Any()); } - internal DataGridColumnCollection CreateColumnsInstance() - { - return new DataGridColumnCollection(this); - } - private void SetValueNoCallback(AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) { _areHandlersSuspended = true; @@ -347,4 +431,23 @@ public partial class DataGrid _areHandlersSuspended = false; } } + + 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(); + } + } } \ No newline at end of file diff --git a/src/AtomUI.Controls.DataGrid/DataGrid.Rows.cs b/src/AtomUI.Controls.DataGrid/DataGrid.Rows.cs index 1b916670d..0a1a13e2b 100644 --- a/src/AtomUI.Controls.DataGrid/DataGrid.Rows.cs +++ b/src/AtomUI.Controls.DataGrid/DataGrid.Rows.cs @@ -1,4 +1,14 @@ -using System.Drawing; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using AtomUI.Controls.Data; +using AtomUI.Controls.Utils; +using AtomUI.Utils; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Media; +using Avalonia.Utilities; namespace AtomUI.Controls; @@ -9,22 +19,17 @@ public partial class DataGrid // 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 double HorizontalAdjustment { get; private set; } + private Size? _rowsPresenterAvailableSize; + internal Size? RowsPresenterAvailableSize { - get - { - return _rowsPresenterAvailableSize; - } + get => _rowsPresenterAvailableSize; set { - if (_rowsPresenterAvailableSize.HasValue && value.HasValue && value.Value.Width > RowsPresenterAvailableSize.Value.Width) + 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 @@ -44,6 +49,7 @@ public partial class DataGrid { HorizontalAdjustment = 0; } + _rowsPresenterAvailableSize = value; } } @@ -56,13 +62,14 @@ public partial class DataGrid { return 0; } + return !double.IsNaN(RowHeaderWidth) ? RowHeaderWidth : RowHeadersDesiredWidth; } } internal bool AreRowHeadersVisible => (HeadersVisibility & DataGridHeadersVisibility.Row) == DataGridHeadersVisibility.Row; - + internal double RowHeadersDesiredWidth { get => _rowHeaderDesiredWidth; @@ -73,12 +80,3177 @@ public partial class DataGrid { double oldActualRowHeaderWidth = ActualRowHeaderWidth; _rowHeaderDesiredWidth = value; - if (oldActualRowHeaderWidth != ActualRowHeaderWidth) + if (!MathUtils.AreClose(oldActualRowHeaderWidth, ActualRowHeaderWidth)) { EnsureRowHeaderWidth(); } } } } + + internal bool AreRowBottomGridLinesRequired => + (GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || + GridLinesVisibility == DataGridGridLinesVisibility.All) && HorizontalGridLinesBrush != null; + + internal int FirstVisibleSlot => (SlotCount > 0) ? GetNextVisibleSlot(-1) : -1; + + internal int FrozenColumnCountWithFiller + { + get + { + int count = FrozenColumnCount; + if (ColumnsInternal.RowGroupSpacerColumn.IsRepresented && (AreRowGroupHeadersFrozen || count > 0)) + { + // Either the RowGroupHeaders are frozen by default or the user set a frozen column count. In both cases, we need to freeze + // one more column than the what the public value says + count++; + } + + return count; + } + } + + internal int LastVisibleSlot => (SlotCount > 0) ? GetPreviousVisibleSlot(SlotCount) : -1; + + internal DataGridRow? EditingRow { get; private set; } + + internal bool LoadingOrUnloadingRow { get; private set; } + + internal double[] RowGroupSublevelIndents + { + get; + private set; + } + #endregion + + + // Cumulated height of all known rows, including the gridlines and details section. + // This property returns an approximation of the actual total row heights and also + // updates the RowHeightEstimate + private double EdgedRowsHeightCalculated + { + get + { + // If we're not displaying any rows or if we have infinite space the, relative height of our rows is 0 + if (DisplayData.LastScrollingSlot == -1 || double.IsPositiveInfinity(AvailableSlotElementRoom)) + { + return 0; + } + + Debug.Assert(DisplayData.LastScrollingSlot >= 0); + Debug.Assert(_verticalOffset >= 0); + Debug.Assert(NegVerticalOffset >= 0); + + // Height of all rows above the viewport + double totalRowsHeight = _verticalOffset - NegVerticalOffset; + + // Add the height of all the rows currently displayed, AvailableRowRoom + // is not always up to date enough for this + foreach (Control element in DisplayData.GetScrollingElements()) + { + if (element is DataGridRow row) + { + totalRowsHeight += row.TargetHeight; + } + else + { + totalRowsHeight += element.DesiredSize.Height; + } + } + + // Details up to and including viewport + int detailsCount = GetDetailsCountInclusive(0, DisplayData.LastScrollingSlot); + + // Subtract details that were accounted for from the totalRowsHeight + totalRowsHeight -= detailsCount * RowDetailsHeightEstimate; + + // Update the RowHeightEstimate if we have more row information + if (DisplayData.LastScrollingSlot >= _lastEstimatedRow) + { + _lastEstimatedRow = DisplayData.LastScrollingSlot; + RowHeightEstimate = totalRowsHeight / + (_lastEstimatedRow + 1 - _collapsedSlotsTable.GetIndexCount(0, _lastEstimatedRow)); + } + + // Calculate estimates for what's beyond the viewport + if (VisibleSlotCount > DisplayData.NumDisplayedScrollingElements) + { + int remainingRowCount = (SlotCount - DisplayData.LastScrollingSlot - + _collapsedSlotsTable.GetIndexCount(DisplayData.LastScrollingSlot, + SlotCount - 1) - 1); + + // Add estimation for the cell heights of all rows beyond our viewport + totalRowsHeight += RowHeightEstimate * remainingRowCount; + + // Add the rest of the details beyond the viewport + detailsCount += GetDetailsCountInclusive(DisplayData.LastScrollingSlot + 1, SlotCount - 1); + } + + // + double totalDetailsHeight = detailsCount * RowDetailsHeightEstimate; + + return totalRowsHeight + totalDetailsHeight; + } + } + + /// + /// Clears the entire selection. Displayed rows are deselected explicitly to visualize + /// potential transition effects + /// + internal void ClearRowSelection(bool resetAnchorSlot) + { + if (resetAnchorSlot) + { + AnchorSlot = -1; + } + + if (_selectedItems.Count > 0) + { + _noSelectionChangeCount++; + try + { + // Individually deselecting displayed rows to view potential transitions + for (int slot = DisplayData.FirstScrollingSlot; + slot > -1 && slot <= DisplayData.LastScrollingSlot; + slot++) + { + if (DisplayData.GetDisplayedElement(slot) is DataGridRow row) + { + if (_selectedItems.ContainsSlot(row.Slot)) + { + SelectSlot(row.Slot, false); + } + } + } + + _selectedItems.ClearRows(); + SelectionHasChanged = true; + } + finally + { + NoSelectionChangeCount--; + } + } + } + + /// + /// Clears the entire selection except the indicated row. Displayed rows are deselected explicitly to + /// visualize potential transition effects. The row indicated is selected if it is not already. + /// + internal void ClearRowSelection(int slotException, bool setAnchorSlot) + { + _noSelectionChangeCount++; + try + { + bool exceptionAlreadySelected = false; + if (_selectedItems.Count > 0) + { + // Individually deselecting displayed rows to view potential transitions + for (int slot = DisplayData.FirstScrollingSlot; + slot > -1 && slot <= DisplayData.LastScrollingSlot; + slot++) + { + if (slot != slotException && _selectedItems.ContainsSlot(slot)) + { + SelectSlot(slot, false); + SelectionHasChanged = true; + } + } + + exceptionAlreadySelected = _selectedItems.ContainsSlot(slotException); + int selectedCount = _selectedItems.Count; + if (selectedCount > 0) + { + if (selectedCount > 1) + { + SelectionHasChanged = true; + } + else + { + int currentlySelectedSlot = _selectedItems.GetIndexes().First(); + if (currentlySelectedSlot != slotException) + { + SelectionHasChanged = true; + } + } + + _selectedItems.ClearRows(); + } + } + + if (exceptionAlreadySelected) + { + // Exception row was already selected. It just needs to be marked as selected again. + // No transition involved. + _selectedItems.SelectSlot(slotException, true /*select*/); + if (setAnchorSlot) + { + AnchorSlot = slotException; + } + } + else + { + // Exception row was not selected. It needs to be selected with potential transition + SetRowSelection(slotException, true /*isSelected*/, setAnchorSlot); + } + } + finally + { + NoSelectionChangeCount--; + } + } + + internal int GetCollapsedSlotCount(int startSlot, int endSlot) + { + return _collapsedSlotsTable.GetIndexCount(startSlot, endSlot); + } + + internal int GetNextVisibleSlot(int slot) + { + return _collapsedSlotsTable.GetNextGap(slot); + } + + internal int GetPreviousVisibleSlot(int slot) + { + return _collapsedSlotsTable.GetPreviousGap(slot); + } + + /// + /// Returns the row associated to the provided backend data item. + /// + /// backend data item + /// null if the DataSource is null, the provided item in not in the source, or the item is not displayed; otherwise, the associated Row + internal DataGridRow? GetRowFromItem(object dataItem) + { + int rowIndex = DataConnection.IndexOf(dataItem); + if (rowIndex < 0) + { + return null; + } + + int slot = SlotFromRowIndex(rowIndex); + return IsSlotVisible(slot) ? DisplayData.GetDisplayedElement(slot) as DataGridRow : null; + } + + internal bool GetRowSelection(int slot) + { + Debug.Assert(slot != -1); + return _selectedItems.ContainsSlot(slot); + } + + internal void InsertElementAt(int slot, int rowIndex, object? item, DataGridRowGroupInfo? groupInfo, bool isCollapsed) + { + Debug.Assert(slot >= 0 && slot <= SlotCount); + + bool isRow = rowIndex != -1; + if (isCollapsed) + { + InsertElement(slot, + element: null, + updateVerticalScrollBarOnly: true, + isCollapsed: true, + isRow: isRow); + } + else if (SlotIsDisplayed(slot)) + { + // Row at that index needs to be displayed + if (isRow) + { + InsertElement(slot, GenerateRow(rowIndex, slot, item), false /*updateVerticalScrollBarOnly*/, + false /*isCollapsed*/, isRow); + } + else + { + InsertElement(slot, GenerateRowGroupHeader(slot, groupInfo), + updateVerticalScrollBarOnly: false, + isCollapsed: false, + isRow: isRow); + } + } + else + { + InsertElement(slot, + element: null, + updateVerticalScrollBarOnly: _vScrollBar == null || _vScrollBar.IsVisible, + isCollapsed: false, + isRow: isRow); + } + } + + internal void InsertRowAt(int rowIndex) + { + int slot = SlotFromRowIndex(rowIndex); + object? item = DataConnection.GetDataItem(rowIndex); + + Debug.Assert(item != null); + + // isCollapsed below is always false because we only use the method if we're not grouping + InsertElementAt(slot, rowIndex, item, null /*DataGridRowGroupInfo*/, false /*isCollapsed*/); + } + + internal bool IsColumnDisplayed(int columnIndex) + { + return columnIndex >= FirstDisplayedNonFillerColumnIndex && + columnIndex <= DisplayData.LastTotallyDisplayedScrollingCol; + } + + internal bool IsRowRecyclable(DataGridRow row) + { + return (row != EditingRow && row != _focusedRow); + } + + internal bool IsSlotVisible(int slot) + { + return slot >= DisplayData.FirstScrollingSlot + && slot <= DisplayData.LastScrollingSlot + && slot != -1 + && !_collapsedSlotsTable.Contains(slot); + } + + internal void OnRowsMeasure() + { + if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight)) + { + ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight); + DisplayData.PendingVerticalScrollHeight = 0; + } + } + + internal void RefreshRows(bool recycleRows, bool clearRows) + { + if (_measured) + { + // _desiredCurrentColumnIndex is used in MakeFirstDisplayedCellCurrentCell to set the + // column position back to what it was before the refresh + _desiredCurrentColumnIndex = CurrentColumnIndex; + double verticalOffset = _verticalOffset; + if (DisplayData.PendingVerticalScrollHeight > 0) + { + // Use the pending vertical scrollbar position if there is one, in the case that the collection + // has been reset multiple times in a row. + verticalOffset = DisplayData.PendingVerticalScrollHeight; + } + + _verticalOffset = 0; + NegVerticalOffset = 0; + + if (clearRows) + { + ClearRows(recycleRows); + ClearRowGroupHeadersTable(); + PopulateRowGroupHeadersTable(); + } + + RefreshRowGroupHeaders(); + + // Update the CurrentSlot because it might have changed + if (recycleRows && DataConnection.CollectionView != null) + { + CurrentSlot = DataConnection.CollectionView.CurrentPosition == -1 + ? -1 + : SlotFromRowIndex(DataConnection.CollectionView.CurrentPosition); + if (CurrentSlot == -1) + { + SetCurrentCellCore(-1, -1); + } + } + + if (ColumnsItemsInternal.Count > 0) + { + AddSlots(DataConnection.Count); + AddSlots(DataConnection.Count + RowGroupHeadersTable.IndexCount); + + InvalidateMeasure(); + } + + EnsureRowGroupSpacerColumn(); + + if (VerticalScrollBar != null) + { + DisplayData.PendingVerticalScrollHeight = Math.Min(verticalOffset, VerticalScrollBar.Maximum); + } + } + else + { + if (clearRows) + { + ClearRows(recycleRows); + } + + ClearRowGroupHeadersTable(); + PopulateRowGroupHeadersTable(); + } + } + + internal void RemoveRowAt(int rowIndex, object item) + { + RemoveElementAt(SlotFromRowIndex(rowIndex), item, true); + } + + internal int RowIndexFromSlot(int slot) + { + return slot - RowGroupHeadersTable.GetIndexCount(0, slot); + } + + internal bool ScrollSlotIntoView(int slot, bool scrolledHorizontally) + { + Debug.Assert(_collapsedSlotsTable.Contains(slot) || !IsSlotOutOfBounds(slot)); + + if (scrolledHorizontally && DisplayData.FirstScrollingSlot <= slot && DisplayData.LastScrollingSlot >= slot) + { + // If the slot is displayed and we scrolled horizontally, column virtualization could cause the rows to grow. + // As a result we need to force measure on the rows we're displaying and recalculate our First and Last slots + // so they're accurate + foreach (var row in DisplayData.GetScrollingRows()) + { + row.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + } + + UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsEstimatedHeight); + } + + if (DisplayData.FirstScrollingSlot < slot && + (DisplayData.LastScrollingSlot > slot || DisplayData.LastScrollingSlot == -1)) + { + // The row is already displayed in its entirety + return true; + } + else if (DisplayData.FirstScrollingSlot == slot && slot != -1) + { + if (!MathUtilities.IsZero(NegVerticalOffset)) + { + // First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0. + DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset; + InvalidateRowsMeasure(false /*invalidateIndividualRows*/); + } + + return true; + } + + double deltaY = 0; + int firstFullSlot; + if (DisplayData.FirstScrollingSlot > slot) + { + // Scroll up to the new row so it becomes the first displayed row + firstFullSlot = DisplayData.FirstScrollingSlot - 1; + if (MathUtilities.GreaterThan(NegVerticalOffset, 0)) + { + deltaY = -NegVerticalOffset; + } + + deltaY -= GetSlotElementsHeight(slot, firstFullSlot); + if (DisplayData.FirstScrollingSlot - slot > 1) + { + // + + ResetDisplayedRows(); + } + + NegVerticalOffset = 0; + UpdateDisplayedRows(slot, CellsEstimatedHeight); + } + else if (DisplayData.LastScrollingSlot <= slot) + { + // Scroll down to the new row so it's entirely displayed. If the height of the row + // is greater than the height of the DataGrid, then show the top of the row at the top + // of the grid + firstFullSlot = DisplayData.LastScrollingSlot; + // Figure out how much of the last row is cut off + double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot); + double availableHeight = AvailableSlotElementRoom + rowHeight; + if (MathUtilities.AreClose(rowHeight, availableHeight)) + { + if (DisplayData.LastScrollingSlot == slot) + { + // We're already at the very bottom so we don't need to scroll down further + return true; + } + else + { + // We're already showing the entire last row so don't count it as part of the delta + firstFullSlot++; + } + } + else if (rowHeight > availableHeight) + { + firstFullSlot++; + deltaY += rowHeight - availableHeight; + } + + // sum up the height of the rest of the full rows + if (slot >= firstFullSlot) + { + deltaY += GetSlotElementsHeight(firstFullSlot, slot); + } + + // If the first row we're displaying is no longer adjacent to the rows we have + // simply discard the ones we have + if (slot - DisplayData.LastScrollingSlot > 1) + { + ResetDisplayedRows(); + } + + if (MathUtilities.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsEstimatedHeight)) + { + // The entire row won't fit in the DataGrid so we start showing it from the top + NegVerticalOffset = 0; + UpdateDisplayedRows(slot, CellsEstimatedHeight); + } + else + { + UpdateDisplayedRowsFromBottom(slot); + } + } + + _verticalOffset += deltaY; + if (_verticalOffset < 0 || DisplayData.FirstScrollingSlot == 0) + { + // We scrolled too far because a row's height was larger than its approximation + _verticalOffset = NegVerticalOffset; + } + + // + Debug.Assert(MathUtilities.LessThanOrClose(NegVerticalOffset, _verticalOffset)); + + SetVerticalOffset(_verticalOffset); + + InvalidateMeasure(); + InvalidateRowsMeasure(false /*invalidateIndividualRows*/); + + return true; + } + + internal void SetRowSelection(int slot, bool isSelected, bool setAnchorSlot) + { + Debug.Assert(!(!isSelected && setAnchorSlot)); + Debug.Assert(!IsSlotOutOfSelectionBounds(slot)); + _noSelectionChangeCount++; + try + { + if (SelectionMode == DataGridSelectionMode.Single && isSelected) + { + Debug.Assert(_selectedItems.Count <= 1); + if (_selectedItems.Count > 0) + { + int currentlySelectedSlot = _selectedItems.GetIndexes().First(); + if (currentlySelectedSlot != slot) + { + SelectSlot(currentlySelectedSlot, false); + SelectionHasChanged = true; + } + } + } + + if (_selectedItems.ContainsSlot(slot) != isSelected) + { + SelectSlot(slot, isSelected); + SelectionHasChanged = true; + } + + if (setAnchorSlot) + { + AnchorSlot = slot; + } + } + finally + { + NoSelectionChangeCount--; + } + } + + // For now, all scenarios are for isSelected == true. + internal void SetRowsSelection(int startSlot, int endSlot /*, bool isSelected*/) + { + Debug.Assert(startSlot >= 0 && startSlot < SlotCount); + Debug.Assert(endSlot >= 0 && endSlot < SlotCount); + Debug.Assert(startSlot <= endSlot); + + _noSelectionChangeCount++; + try + { + if ( /*isSelected &&*/ !_selectedItems.ContainsAll(startSlot, endSlot)) + { + // At least one row gets selected + SelectSlots(startSlot, endSlot, true); + SelectionHasChanged = true; + } + } + finally + { + NoSelectionChangeCount--; + } + } + + internal int SlotFromRowIndex(int rowIndex) + { + return rowIndex + RowGroupHeadersTable.GetIndexCountBeforeGap(0, rowIndex); + } + + private void AddSlotElement(int slot, Control element) + { +#if DEBUG + if (element is DataGridRow row) + { + Debug.Assert(row.OwningGrid == this); + Debug.Assert(row.Cells.Count == ColumnsItemsInternal.Count); + + int columnIndex = 0; + foreach (DataGridCell dataGridCell in row.Cells) + { + Debug.Assert(dataGridCell.OwningRow == row); + Debug.Assert(dataGridCell.OwningColumn == ColumnsItemsInternal[columnIndex]); + columnIndex++; + } + } +#endif + Debug.Assert(slot == SlotCount); + + NotifyAddedElementPhase1(slot, element); + SlotCount++; + VisibleSlotCount++; + NotifyAddedElementPhase2(slot, updateVerticalScrollBarOnly: false); + NotifyElementsChanged(grew: true); + } + + private void AddSlots(int totalSlots) + { + SlotCount = 0; + VisibleSlotCount = 0; + IEnumerator? groupSlots = null; + int nextGroupSlot = -1; + try + { + if (RowGroupHeadersTable.RangeCount > 0) + { + groupSlots = RowGroupHeadersTable.GetIndexes().GetEnumerator(); + if (groupSlots.MoveNext()) + { + nextGroupSlot = groupSlots.Current; + } + } + + int slot = 0; + int addedRows = 0; + while (slot < totalSlots && AvailableSlotElementRoom > 0) + { + if (slot == nextGroupSlot) + { + Debug.Assert(groupSlots != null); + DataGridRowGroupInfo? groupRowInfo = RowGroupHeadersTable.GetValueAt(slot); + AddSlotElement(slot, GenerateRowGroupHeader(slot, groupRowInfo)); + nextGroupSlot = groupSlots.MoveNext() ? groupSlots.Current : -1; + } + else + { + AddSlotElement(slot, GenerateRow(addedRows, slot)); + addedRows++; + } + + slot++; + } + + if (slot < totalSlots) + { + SlotCount += totalSlots - slot; + VisibleSlotCount += totalSlots - slot; + NotifyAddedElementPhase2(0, + updateVerticalScrollBarOnly: _vScrollBar == null || _vScrollBar.IsVisible); + NotifyElementsChanged(grew: true); + } + } + finally + { + if (groupSlots is IDisposable disposable) + { + disposable.Dispose(); + } + } + + } + + private void ApplyDisplayedRowsState(int startSlot, int endSlot) + { + int firstSlot = Math.Max(DisplayData.FirstScrollingSlot, startSlot); + int lastSlot = Math.Min(DisplayData.LastScrollingSlot, endSlot); + + if (firstSlot >= 0) + { + Debug.Assert(lastSlot >= firstSlot); + int slot = GetNextVisibleSlot(firstSlot - 1); + while (slot <= lastSlot) + { + if (DisplayData.GetDisplayedElement(slot) is DataGridRow row) + { + row.ApplyState(); + } + + slot = GetNextVisibleSlot(slot); + } + } + } + + private void ClearRows(bool recycle) + { + // Need to clean up recycled rows even if the RowCount is 0 + SetCurrentCellCore(-1, -1, commitEdit: false, endRowEdit: false); + ClearRowSelection(resetAnchorSlot: true); + UnloadElements(recycle); + + _showDetailsTable.Clear(); + SlotCount = 0; + NegVerticalOffset = 0; + SetVerticalOffset(0); + ComputeScrollBarsLayout(); + } + + // Updates _collapsedSlotsTable and returns the number of pixels that were collapsed + private double CollapseSlotsInTable(int startSlot, int endSlot, ref int slotsExpanded, int lastDisplayedSlot, + ref double heightChangeBelowLastDisplayedSlot) + { + int firstSlot = startSlot; + int lastSlot; + double totalHeightChange = 0; + // Figure out which slots actually need to be expanded since some might already be collapsed + while (firstSlot <= endSlot) + { + firstSlot = _collapsedSlotsTable.GetNextGap(firstSlot - 1); + int nextCollapsedSlot = _collapsedSlotsTable.GetNextIndex(firstSlot) - 1; + lastSlot = nextCollapsedSlot == -2 ? endSlot : Math.Min(endSlot, nextCollapsedSlot); + + if (firstSlot <= lastSlot) + { + double heightChange = GetHeightEstimate(firstSlot, lastSlot); + totalHeightChange -= heightChange; + slotsExpanded -= lastSlot - firstSlot + 1; + + if (lastSlot > lastDisplayedSlot) + { + if (firstSlot > lastDisplayedSlot) + { + heightChangeBelowLastDisplayedSlot -= heightChange; + } + else + { + heightChangeBelowLastDisplayedSlot -= GetHeightEstimate(lastDisplayedSlot + 1, lastSlot); + } + } + + firstSlot = lastSlot + 1; + } + } + + // Update _collapsedSlotsTable in one bulk operation + _collapsedSlotsTable.AddValues(startSlot, endSlot - startSlot + 1, false); + + return totalHeightChange; + } + + private static void CorrectRowAfterDeletion(DataGridRow row, bool rowDeleted) + { + row.Slot--; + if (rowDeleted) + { + row.Index--; + } + } + + private static void CorrectRowAfterInsertion(DataGridRow row, bool rowInserted) + { + row.Slot++; + if (rowInserted) + { + row.Index++; + } + } + + /// + /// Adjusts the index of all displayed, loaded and edited rows after a row was deleted. + /// Removes the deleted row from the list of loaded rows if present. + /// + private void CorrectSlotsAfterDeletion(int slotDeleted, bool wasRow) + { + Debug.Assert(slotDeleted >= 0); + + // Take care of the non-visible loaded rows + for (int index = 0; index < _loadedRows.Count;) + { + DataGridRow dataGridRow = _loadedRows[index]; + if (IsSlotVisible(dataGridRow.Slot)) + { + index++; + } + else + { + if (dataGridRow.Slot > slotDeleted) + { + CorrectRowAfterDeletion(dataGridRow, wasRow); + index++; + } + else if (dataGridRow.Slot == slotDeleted) + { + _loadedRows.RemoveAt(index); + } + else + { + index++; + } + } + } + + // Take care of the non-visible edited row + if (EditingRow != null && + !IsSlotVisible(EditingRow.Slot) && + EditingRow.Slot > slotDeleted) + { + CorrectRowAfterDeletion(EditingRow, wasRow); + } + + // Take care of the non-visible focused row + if (_focusedRow != null && + _focusedRow != EditingRow && + !IsSlotVisible(_focusedRow.Slot) && + _focusedRow.Slot > slotDeleted) + { + CorrectRowAfterDeletion(_focusedRow, wasRow); + } + + // Take care of the visible rows + foreach (var control in DisplayData.GetScrollingRows()) + { + if (control is DataGridRow row) + { + if (row.Slot > slotDeleted) + { + CorrectRowAfterDeletion(row, wasRow); + _rowsPresenter?.InvalidateChildIndex(row); + } + } + } + + // Update the RowGroupHeaders + foreach (int slot in RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo? rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot); + Debug.Assert(rowGroupInfo != null); + if (rowGroupInfo.Slot > slotDeleted) + { + rowGroupInfo.Slot--; + } + + if (rowGroupInfo.LastSubItemSlot >= slotDeleted) + { + rowGroupInfo.LastSubItemSlot--; + } + } + + // Update which row we've calculated the RowHeightEstimate up to + if (_lastEstimatedRow >= slotDeleted) + { + _lastEstimatedRow--; + } + } + + /// + /// Adjusts the index of all displayed, loaded and edited rows after rows were deleted. + /// + private void CorrectSlotsAfterInsertion(int slotInserted, bool isCollapsed, bool rowInserted) + { + Debug.Assert(slotInserted >= 0); + + // Take care of the non-visible loaded rows + foreach (DataGridRow dataGridRow in _loadedRows) + { + if (!IsSlotVisible(dataGridRow.Slot) && dataGridRow.Slot >= slotInserted) + { + CorrectRowAfterInsertion(dataGridRow, rowInserted); + } + } + + // Take care of the non-visible focused row + if (_focusedRow != null && + _focusedRow != EditingRow && + !(IsSlotVisible(_focusedRow.Slot) || ((_focusedRow.Slot == slotInserted) && isCollapsed)) && + _focusedRow.Slot >= slotInserted) + { + CorrectRowAfterInsertion(_focusedRow, rowInserted); + } + + // Take care of the visible rows + foreach (var control in DisplayData.GetScrollingRows()) + { + if (control is DataGridRow row) + { + if (row.Slot >= slotInserted) + { + CorrectRowAfterInsertion(row, rowInserted); + _rowsPresenter?.InvalidateChildIndex(row); + } + } + } + + // Re-calculate the EditingRow's Slot and Index and ensure that it is still selected. + if (EditingRow != null) + { + EditingRow.Index = DataConnection.IndexOf(EditingRow.DataContext); + EditingRow.Slot = SlotFromRowIndex(EditingRow.Index); + } + + // Update the RowGroupHeaders + foreach (int slot in RowGroupHeadersTable.GetIndexes(slotInserted)) + { + DataGridRowGroupInfo? rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot); + Debug.Assert(rowGroupInfo != null); + if (rowGroupInfo.Slot >= slotInserted) + { + rowGroupInfo.Slot++; + } + + // We are purposefully checking GT and not GTE because the equality case is handled + // by the CorrectLastSubItemSlotsAfterInsertion method + if (rowGroupInfo.LastSubItemSlot > slotInserted) + { + rowGroupInfo.LastSubItemSlot++; + } + } + + // Update which row we've calculated the RowHeightEstimate up to + if (_lastEstimatedRow >= slotInserted) + { + _lastEstimatedRow++; + } + } + + private IEnumerable GetAllRows() + { + if (_rowsPresenter != null) + { + foreach (Control element in _rowsPresenter.Children) + { + if (element is DataGridRow row) + { + yield return row; + } + } + } + } + + // Expands slots from startSlot to endSlot inclusive and adds the amount expanded in this suboperation to + // the given totalHeightChanged of the entire operation + private void ExpandSlots(int startSlot, int endSlot, bool isDisplayed, ref int slotsExpanded, + ref double totalHeightChange) + { + double heightAboveStartSlot = 0; + if (isDisplayed) + { + int slot = DisplayData.FirstScrollingSlot; + while (slot < startSlot) + { + heightAboveStartSlot += GetExactSlotElementHeight(slot); + slot = GetNextVisibleSlot(slot); + } + + // First make the bottom rows available for recycling so we minimize element creation when expanding + for (int i = 0; (i < endSlot - startSlot + 1) && (DisplayData.LastScrollingSlot > endSlot); i++) + { + RemoveDisplayedElement(DisplayData.LastScrollingSlot, wasDeleted: false, updateSlotInformation: true); + } + } + + // Figure out which slots actually need to be expanded since some might already be collapsed + double currentHeightChange = 0; + int firstSlot = startSlot; + int lastSlot = endSlot; + while (firstSlot <= endSlot) + { + firstSlot = _collapsedSlotsTable.GetNextIndex(firstSlot - 1); + if (firstSlot == -1) + { + break; + } + + lastSlot = Math.Min(endSlot, _collapsedSlotsTable.GetNextGap(firstSlot) - 1); + + if (firstSlot <= lastSlot) + { + if (!isDisplayed) + { + // Estimate the height change if the slots aren't displayed. If they are displayed, we can add real values + double rowCount = lastSlot - firstSlot - + GetRowGroupHeaderCount(firstSlot, lastSlot, false, out double headerHeight) + 1; + double detailsCount = GetDetailsCountInclusive(firstSlot, lastSlot); + currentHeightChange += headerHeight + (detailsCount * RowDetailsHeightEstimate) + + (rowCount * RowHeightEstimate); + } + + slotsExpanded += lastSlot - firstSlot + 1; + firstSlot = lastSlot + 1; + } + } + + // Update _collapsedSlotsTable in one bulk operation + _collapsedSlotsTable.RemoveValues(startSlot, endSlot - startSlot + 1); + + if (isDisplayed) + { + double availableHeight = CellsEstimatedHeight - heightAboveStartSlot; + // Actually expand the displayed slots up to what we can display + for (int i = startSlot; (i <= endSlot) && (currentHeightChange < availableHeight); i++) + { + Control insertedElement = InsertDisplayedElement(i, updateSlotInformation: false); + currentHeightChange += insertedElement.DesiredSize.Height; + if (i > DisplayData.LastScrollingSlot) + { + DisplayData.LastScrollingSlot = i; + } + } + } + + // Update the total height for the entire Expand operation + totalHeightChange += currentHeightChange; + } + + /// + /// Creates all the editing elements for the current editing row, so the bindings + /// all exist during validation. + /// + private void GenerateEditingElements() + { + if (EditingRow != null) + { + Debug.Assert(EditingRow.Cells.Count == ColumnsItemsInternal.Count); + Debug.Assert(EditingRow.DataContext != null); + foreach (DataGridColumn column in ColumnsInternal.GetDisplayedColumns(c => c.IsVisible && !c.IsReadOnly)) + { + column.GenerateEditingElementInternal(EditingRow.Cells[column.Index], EditingRow.DataContext); + } + } + } + + /// + /// Returns a row for the provided index. The row gets first loaded through the LoadingRow event. + /// + private DataGridRow GenerateRow(int rowIndex, int slot) + { + return GenerateRow(rowIndex, slot, DataConnection.GetDataItem(rowIndex)); + } + + /// + /// Returns a row for the provided index. The row gets first loaded through the LoadingRow event. + /// + private DataGridRow GenerateRow(int rowIndex, int slot, object? dataContext) + { + Debug.Assert(rowIndex > -1); + DataGridRow? dataGridRow = GetGeneratedRow(dataContext); + if (dataGridRow == null) + { + dataGridRow = DisplayData.GetUsedRow() ?? new DataGridRow(); + dataGridRow.Index = rowIndex; + dataGridRow.Slot = slot; + dataGridRow.OwningGrid = this; + dataGridRow.DataContext = dataContext; + if (RowTheme is { } rowTheme) + { + dataGridRow.SetValue(ThemeProperty, rowTheme, BindingPriority.Template); + } + + CompleteCellsCollection(dataGridRow); + + OnLoadingRow(new DataGridRowEventArgs(dataGridRow)); + } + + return dataGridRow; + } + + /// + /// Returns the exact row height, whether it is currently displayed or not. + /// The row is generated and added to the displayed rows in case it is not already displayed. + /// The horizontal gridlines thickness are added. + /// + private double GetExactSlotElementHeight(int slot) + { + Debug.Assert((slot >= 0) && slot < SlotCount); + + if (IsSlotVisible(slot)) + { + Debug.Assert(DisplayData.GetDisplayedElement(slot) != null); + return DisplayData.GetDisplayedElement(slot).DesiredSize.Height; + } + + Control slotElement = InsertDisplayedElement(slot, true /*updateSlotInformation*/); + Debug.Assert(slotElement != null); + return slotElement.DesiredSize.Height; + } + + // Returns an estimate for the height of the slots between fromSlot and toSlot + private double GetHeightEstimate(int fromSlot, int toSlot) + { + double rowCount = toSlot - fromSlot - GetRowGroupHeaderCount(fromSlot, toSlot, true, out double headerHeight) + + 1; + double detailsCount = GetDetailsCountInclusive(fromSlot, toSlot); + + return headerHeight + (detailsCount * RowDetailsHeightEstimate) + (rowCount * RowHeightEstimate); + } + + /// + /// If the provided slot is displayed, returns the exact height. + /// If the slot is not displayed, returns a default height. + /// + private double GetSlotElementHeight(int slot) + { + Debug.Assert(slot >= 0 && slot < SlotCount); + if (IsSlotVisible(slot)) + { + Debug.Assert(DisplayData.GetDisplayedElement(slot) != null); + return DisplayData.GetDisplayedElement(slot).DesiredSize.Height; + } + else + { + DataGridRowGroupInfo? rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot); + if (rowGroupInfo != null) + { + return _rowGroupHeightsByLevel[rowGroupInfo.Level]; + } + + // Assume it's a row since we're either not grouping or it wasn't a RowGroupHeader + return RowHeightEstimate + (GetRowDetailsVisibility(slot) ? RowDetailsHeightEstimate : 0); + } + } + + /// + /// Cumulates the approximate height of the rows from fromRowIndex to toRowIndex included. + /// Including the potential gridline thickness. + /// + private double GetSlotElementsHeight(int fromSlot, int toSlot) + { + Debug.Assert(toSlot >= fromSlot); + + double height = 0; + for (int slot = fromSlot; slot <= toSlot; slot++) + { + height += GetSlotElementHeight(slot); + } + + return height; + } + + /// + /// Checks if the row for the provided dataContext has been generated and is present + /// in either the loaded rows, pre-fetched rows, or editing row. + /// The displayed rows are *not* searched. Returns null if the row does not belong to those 3 categories. + /// + private DataGridRow? GetGeneratedRow(object? dataContext) + { + // Check the list of rows being loaded via the LoadingRow event. + DataGridRow? dataGridRow = GetLoadedRow(dataContext); + if (dataGridRow != null) + { + return dataGridRow; + } + + // Check the potential editing row. + if (EditingRow != null && dataContext == EditingRow.DataContext) + { + return EditingRow; + } + + // Check the potential focused row. + if (_focusedRow != null && dataContext == _focusedRow.DataContext) + { + return _focusedRow; + } + + return null; + } + + private DataGridRow? GetLoadedRow(object? dataContext) + { + foreach (DataGridRow dataGridRow in _loadedRows) + { + if (dataGridRow.DataContext == dataContext) + { + return dataGridRow; + } + } + + return null; + } + + private Control InsertDisplayedElement(int slot, bool updateSlotInformation) + { + Control slotElement; + if (RowGroupHeadersTable.Contains(slot)) + { + slotElement = GenerateRowGroupHeader(slot, rowGroupInfo: RowGroupHeadersTable.GetValueAt(slot)); + } + else + { + // If we're grouping, the GroupLevel needs to be fixed later by methods calling this + // which end up inserting rows. We don't do it here because elements could be inserted + // from top to bottom or bottom to up so it's better to do in one pass + slotElement = GenerateRow(RowIndexFromSlot(slot), slot); + } + + InsertDisplayedElement(slot, slotElement, wasNewlyAdded: false, updateSlotInformation: updateSlotInformation); + return slotElement; + } + + private void InsertDisplayedElement(int slot, Control element, bool wasNewlyAdded, bool updateSlotInformation) + { + // We can only support creating new rows that are adjacent to the currently visible rows + // since they need to be added to the visual tree for us to Measure them. + Debug.Assert(DisplayData.FirstScrollingSlot == -1 || + slot >= GetPreviousVisibleSlot(DisplayData.FirstScrollingSlot) && + slot <= GetNextVisibleSlot(DisplayData.LastScrollingSlot)); + Debug.Assert(element != null); + + if (_rowsPresenter != null) + { + DataGridRowGroupHeader? groupHeader = null; + DataGridRow? row = element as DataGridRow; + if (row != null) + { + LoadRowVisualsForDisplay(row); + + if (IsRowRecyclable(row)) + { + if (!row.IsRecycled) + { + Debug.Assert(!_rowsPresenter.Children.Contains(element)); + _rowsPresenter.Children.Add(row); + } + } + else + { + element.Clip = null; + Debug.Assert(row.Index == RowIndexFromSlot(slot)); + } + } + else + { + groupHeader = element as DataGridRowGroupHeader; + Debug.Assert(groupHeader != null); // Nothing other and Rows and RowGroups now + groupHeader.TotalIndent = + (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1]; + if (!groupHeader.IsRecycled) + { + _rowsPresenter.Children.Add(element); + } + + groupHeader.LoadVisualsForDisplay(); + } + + // Measure the element and update AvailableRowRoom + element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + AvailableSlotElementRoom -= element.DesiredSize.Height; + + if (groupHeader != null) + { + _rowGroupHeightsByLevel[groupHeader.Level] = groupHeader.DesiredSize.Height; + } + + if (row != null && MathUtils.AreClose(RowHeightEstimate, DefaultRowHeight) && double.IsNaN(row.Height)) + { + RowHeightEstimate = element.DesiredSize.Height; + } + } + + if (wasNewlyAdded) + { + DisplayData.CorrectSlotsAfterInsertion(slot, element, isCollapsed: false); + } + else + { + DisplayData.LoadScrollingSlot(slot, element, updateSlotInformation); + } + } + + private void InsertElement(int slot, Control? element, bool updateVerticalScrollBarOnly, bool isCollapsed, + bool isRow) + { + Debug.Assert(slot >= 0 && slot <= SlotCount); + + NotifyInsertingElement(slot, true /*firstInsertion*/, + isCollapsed); // will throw an exception if the insertion is illegal + + NotifyInsertedElementPhase1(slot, element, isCollapsed, isRow); + SlotCount++; + if (!isCollapsed) + { + VisibleSlotCount++; + } + + NotifyInsertedElementPhase2(slot, updateVerticalScrollBarOnly, isCollapsed); + } + + private void InvalidateRowHeightEstimate() + { + // Start from scratch and assume that we haven't estimated any rows + _lastEstimatedRow = -1; + } + + private void NotifyAddedElementPhase1(int slot, Control element) + { + Debug.Assert(slot >= 0); + + // Row needs to be potentially added to the displayed rows + if (SlotIsDisplayed(slot)) + { + InsertDisplayedElement(slot, element, true /*wasNewlyAdded*/, true); + } + } + + private void NotifyAddedElementPhase2(int slot, bool updateVerticalScrollBarOnly) + { + if (slot < DisplayData.FirstScrollingSlot - 1) + { + // The element was added above our viewport so it pushes the VerticalOffset down + double elementHeight = + RowGroupHeadersTable.Contains(slot) ? RowGroupHeaderHeightEstimate : RowHeightEstimate; + + SetVerticalOffset(_verticalOffset + elementHeight); + } + + if (updateVerticalScrollBarOnly) + { + UpdateVerticalScrollBar(); + } + else + { + ComputeScrollBarsLayout(); + // Reposition rows in case we use a recycled one + InvalidateRowsArrange(); + } + } + + private void NotifyElementsChanged(bool grew) + { + if (grew && + ColumnsItemsInternal.Count > 0 && + CurrentColumnIndex == -1) + { + MakeFirstDisplayedCellCurrentCell(); + } + } + + private void NotifyInsertedElementPhase1(int slot, Control? element, bool isCollapsed, bool isRow) + { + Debug.Assert(slot >= 0); + + // Fix the Index of all following rows + CorrectSlotsAfterInsertion(slot, isCollapsed, isRow); + + // Next, same effect as adding a row + if (element != null) + { +#if DEBUG + if (element is DataGridRow dataGridRow) + { + Debug.Assert(dataGridRow.Cells.Count == ColumnsItemsInternal.Count); + + int columnIndex = 0; + foreach (DataGridCell dataGridCell in dataGridRow.Cells) + { + Debug.Assert(dataGridCell.OwningRow == dataGridRow); + Debug.Assert(dataGridCell.OwningColumn == ColumnsItemsInternal[columnIndex]); + columnIndex++; + } + } +#endif + Debug.Assert(!isCollapsed); + NotifyAddedElementPhase1(slot, element); + } + else if ((slot <= DisplayData.FirstScrollingSlot) || (isCollapsed && (slot <= DisplayData.LastScrollingSlot))) + { + DisplayData.CorrectSlotsAfterInsertion(slot, null /*row*/, isCollapsed); + } + } + + private void NotifyInsertedElementPhase2(int slot, bool updateVerticalScrollBarOnly, bool isCollapsed) + { + Debug.Assert(slot >= 0); + + if (!isCollapsed) + { + // Same effect as adding a row + NotifyAddedElementPhase2(slot, updateVerticalScrollBarOnly); + } + } + + private void NotifyInsertingElement(int slotInserted, + bool firstInsertion, + bool isCollapsed) + { + // Reset the current cell's address if it's after the inserted row. + if (firstInsertion) + { + if (CurrentSlot != -1 && slotInserted <= CurrentSlot) + { + // The underlying data was already added, therefore we need to avoid accessing any back-end data since we might be off by 1 row. + _temporarilyResetCurrentCell = true; + bool success = SetCurrentCellCore(-1, -1); + Debug.Assert(success); + } + } + + _showDetailsTable.InsertIndex(slotInserted); + // Update the slot ranges for the RowGroupHeaders before updating the _selectedItems table, + // because it's dependent on the slots being correct with regards to grouping. + RowGroupHeadersTable.InsertIndex(slotInserted); + _selectedItems.InsertIndex(slotInserted); + + if (isCollapsed) + { + _collapsedSlotsTable.InsertIndexAndValue(slotInserted, false); + } + else + { + _collapsedSlotsTable.InsertIndex(slotInserted); + } + + // If we've inserted rows before the current selected item, update its index + if (slotInserted <= SelectedIndex) + { + SetValueNoCallback(SelectedIndexProperty, SelectedIndex + 1); + } + } + + private void NotifyRemovedElement(int slotDeleted, object? itemDeleted) + { + SlotCount--; + bool wasCollapsed = _collapsedSlotsTable.Contains(slotDeleted); + if (!wasCollapsed) + { + VisibleSlotCount--; + } + + // If we're deleting the focused row, we need to clear the cached value + if (_focusedRow != null && _focusedRow.Slot == slotDeleted) + { + ResetFocusedRow(); + } + + // The element needs to be potentially removed from the displayed elements + Control? elementDeleted = null; + if (slotDeleted <= DisplayData.LastScrollingSlot) + { + if ((slotDeleted >= DisplayData.FirstScrollingSlot) && !wasCollapsed) + { + elementDeleted = DisplayData.GetDisplayedElement(slotDeleted); + // We need to retrieve the Element before updating the tables, but we need + // to update the tables before updating DisplayData in RemoveDisplayedElement + UpdateTablesForRemoval(slotDeleted, itemDeleted); + + // Displayed row is removed + RemoveDisplayedElement(elementDeleted, slotDeleted, true /*wasDeleted*/, + true /*updateSlotInformation*/); + } + else + { + UpdateTablesForRemoval(slotDeleted, itemDeleted); + + // Removed row is not in view, just update the DisplayData + DisplayData.CorrectSlotsAfterDeletion(slotDeleted, wasCollapsed); + } + } + else + { + // The element was removed beyond the viewport so we just need to update the tables + UpdateTablesForRemoval(slotDeleted, itemDeleted); + } + + // If a row was removed before the currently selected row, update its index + if (slotDeleted < SelectedIndex) + { + SetValueNoCallback(SelectedIndexProperty, SelectedIndex - 1); + } + + if (!wasCollapsed) + { + if (slotDeleted >= DisplayData.LastScrollingSlot && elementDeleted == null) + { + // Deleted Row is below our Viewport, we just need to adjust the scrollbar + UpdateVerticalScrollBar(); + } + else + { + if (elementDeleted != null) + { + // Deleted Row is within our Viewport, update the AvailableRowRoom + AvailableSlotElementRoom += elementDeleted.DesiredSize.Height; + } + else + { + // Deleted Row is above our Viewport, update the vertical offset + SetVerticalOffset(Math.Max(0, _verticalOffset - RowHeightEstimate)); + } + + ComputeScrollBarsLayout(); + // Reposition rows in case we use a recycled one + InvalidateRowsArrange(); + } + } + } + + private void OnRemovingElement(int slotDeleted) + { + // Note that the row needs to be deleted no matter what. The underlying data row was already deleted. + + Debug.Assert(slotDeleted >= 0 && slotDeleted < SlotCount); + _temporarilyResetCurrentCell = false; + + // Reset the current cell's address if it's on the deleted row, or after it. + if (CurrentSlot != -1 && slotDeleted <= CurrentSlot) + { + _desiredCurrentColumnIndex = CurrentColumnIndex; + if (slotDeleted == CurrentSlot) + { + // No editing is committed since the underlying entity was already deleted. + bool success = SetCurrentCellCore(-1, -1, false /*commitEdit*/, false /*endRowEdit*/); + Debug.Assert(success); + } + else + { + // Underlying data of deleted row is gone. It cannot be accessed anymore. Skip the commit of the editing. + _temporarilyResetCurrentCell = true; + bool success = SetCurrentCellCore(-1, -1); + Debug.Assert(success); + } + } + } + + // Makes sure the row shows the proper visuals for selection, currency, details, etc. + private void LoadRowVisualsForDisplay(DataGridRow row) + { + // If the row has been recycled, reapply the BackgroundBrush + if (row.IsRecycled) + { + row.ApplyCellsState(); + _rowsPresenter?.InvalidateChildIndex(row); + } + else if (row == EditingRow) + { + row.ApplyCellsState(); + } + + // Set the Row's Style if we one's defined at the DataGrid level and the user didn't + // set one at the row level + //EnsureElementStyle(row, null, RowStyle); + row.EnsureHeaderStyleAndVisibility(null); + + // Check to see if the row contains the CurrentCell, apply its state. + if (CurrentColumnIndex != -1 && + CurrentSlot != -1 && + row.Index == CurrentSlot) + { + row.Cells[CurrentColumnIndex].UpdatePseudoClasses(); + } + + if (row.IsSelected || row.IsRecycled) + { + row.ApplyState(); + } + + // Show or hide RowDetails based on DataGrid settings + EnsureRowDetailsVisibility(row, raiseNotification: false, animate: false); + } + + private void RemoveDisplayedElement(int slot, bool wasDeleted, bool updateSlotInformation) + { + Debug.Assert(slot >= DisplayData.FirstScrollingSlot && + slot <= DisplayData.LastScrollingSlot); + + RemoveDisplayedElement(DisplayData.GetDisplayedElement(slot), slot, wasDeleted, updateSlotInformation); + } + + // Removes an element from display either because it was deleted or it was scrolled out of view. + // If the element was provided, it will be the element removed; otherwise, the element will be + // retrieved from the slot information + private void RemoveDisplayedElement(Control element, int slot, bool wasDeleted, bool updateSlotInformation) + { + if (element is DataGridRow dataGridRow) + { + if (IsRowRecyclable(dataGridRow)) + { + UnloadRow(dataGridRow); + } + else + { + dataGridRow.Clip = new RectangleGeometry(); + } + } + else if (element is DataGridRowGroupHeader groupHeader) + { + OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader)); + DisplayData.AddRecyclableRowGroupHeader(groupHeader); + } + else if (_rowsPresenter != null) + { + _rowsPresenter.Children.Remove(element); + } + + // Update DisplayData + if (wasDeleted) + { + DisplayData.CorrectSlotsAfterDeletion(slot, wasCollapsed: false); + } + else + { + DisplayData.UnloadScrollingElement(slot, updateSlotInformation, wasDeleted: false); + } + } + + /// + /// Removes all of the editing elements for the row that is just leaving editing mode. + /// + private void RemoveEditingElements() + { + if (EditingRow != null) + { + Debug.Assert(EditingRow.Cells.Count == ColumnsItemsInternal.Count); + foreach (DataGridColumn column in Columns) + { + column.RemoveEditingElement(); + } + } + } + + private void RemoveElementAt(int slot, object? item, bool isRow) + { + Debug.Assert(slot >= 0 && slot < SlotCount); + + OnRemovingElement(slot); + + CorrectSlotsAfterDeletion(slot, isRow); + + NotifyRemovedElement(slot, item); + + // Synchronize CurrentCellCoordinates, CurrentColumn, CurrentColumnIndex, CurrentItem + // and CurrentSlot with the currently edited cell, since OnRemovingElement called + // SetCurrentCellCore(-1, -1) to temporarily reset the current cell. + if (_temporarilyResetCurrentCell && + _editingColumnIndex != -1 && + _previousCurrentItem != null && + EditingRow != null && + EditingRow.Slot != -1) + { + ProcessSelectionAndCurrency( + columnIndex: _editingColumnIndex, + item: _previousCurrentItem, + backupSlot: this.EditingRow.Slot, + action: DataGridSelectionAction.None, + scrollIntoView: false); + } + } + + private void RemoveNonDisplayedRows(int newFirstDisplayedSlot, int newLastDisplayedSlot) + { + while (DisplayData.FirstScrollingSlot < newFirstDisplayedSlot) + { + // Need to add rows above the lastDisplayedScrollingRow + RemoveDisplayedElement(DisplayData.FirstScrollingSlot, false /*wasDeleted*/, + true /*updateSlotInformation*/); + } + + while (DisplayData.LastScrollingSlot > newLastDisplayedSlot) + { + // Need to remove rows below the lastDisplayedScrollingRow + RemoveDisplayedElement(DisplayData.LastScrollingSlot, false /*wasDeleted*/, true /*updateSlotInformation*/); + } + } + + private void ResetDisplayedRows() + { + if (UnloadingRow != null || UnloadingRowGroup != null) + { + foreach (Control element in DisplayData.GetScrollingElements()) + { + // Raise Unloading Row for all the rows we're displaying + if (element is DataGridRow row) + { + if (IsRowRecyclable(row)) + { + OnUnloadingRow(new DataGridRowEventArgs(row)); + } + } + // Raise Unloading Row for all the RowGroupHeaders we're displaying + else if (element is DataGridRowGroupHeader groupHeader) + { + OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader)); + } + } + } + + DisplayData.ClearElements(recycle: true); + AvailableSlotElementRoom = CellsEstimatedHeight; + } + + /// + /// Determines whether the row at the provided index must be displayed or not. + /// + private bool SlotIsDisplayed(int slot) + { + Debug.Assert(slot >= 0); + + if (slot >= DisplayData.FirstScrollingSlot && + slot <= DisplayData.LastScrollingSlot) + { + // Additional row takes the spot of a displayed row - it is necessarily displayed + return true; + } + if (DisplayData.FirstScrollingSlot == -1 && + CellsEstimatedHeight > 0 && + CellsWidth > 0) + { + return true; + } + if (slot == GetNextVisibleSlot(DisplayData.LastScrollingSlot)) + { + if (AvailableSlotElementRoom > 0) + { + // There is room for this additional row + return true; + } + } + + return false; + } + + // Updates display information and displayed rows after scrolling the given number of pixels + private void ScrollSlotsByHeight(double height) + { + Debug.Assert(DisplayData.FirstScrollingSlot >= 0); + Debug.Assert(!MathUtilities.IsZero(height)); + + _scrollingByHeight = true; + try + { + double deltaY = 0; + int newFirstScrollingSlot = DisplayData.FirstScrollingSlot; + double newVerticalOffset = _verticalOffset + height; + if (height > 0) + { + // Scrolling Down + int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount); + if (_vScrollBar != null && MathUtilities.AreClose(_vScrollBar.Maximum, newVerticalOffset)) + { + // We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom + // of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by + // OnRowMeasure(Size). For most data, this should be unnoticeable. + ResetDisplayedRows(); + UpdateDisplayedRowsFromBottom(lastVisibleSlot); + newFirstScrollingSlot = DisplayData.FirstScrollingSlot; + } + else + { + deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset; + if (MathUtilities.LessThan(height, deltaY)) + { + // We've merely covered up more of the same row we're on + NegVerticalOffset += height; + } + else + { + // Figure out what row we've scrolled down to and update the value for NegVerticalOffset + NegVerticalOffset = 0; + // + if (height > 2 * CellsEstimatedHeight && + (RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.VisibleWhenSelected || + RowDetailsTemplate == null)) + { + // Very large scroll occurred. Instead of determining the exact number of scrolled off rows, + // let's estimate the number based on RowHeight. + ResetDisplayedRows(); + double singleRowHeightEstimate = RowHeightEstimate + + (RowDetailsVisibilityMode == + DataGridRowDetailsVisibilityMode.Visible + ? RowDetailsHeightEstimate + : 0); + int scrolledToSlot = newFirstScrollingSlot + (int)(height / singleRowHeightEstimate); + scrolledToSlot += _collapsedSlotsTable.GetIndexCount(newFirstScrollingSlot, + newFirstScrollingSlot + scrolledToSlot); + newFirstScrollingSlot = Math.Min(GetNextVisibleSlot(scrolledToSlot), lastVisibleSlot); + } + else + { + while (MathUtilities.LessThanOrClose(deltaY, height)) + { + if (newFirstScrollingSlot < lastVisibleSlot) + { + if (IsSlotVisible(newFirstScrollingSlot)) + { + // Make the top row available for reuse + RemoveDisplayedElement(newFirstScrollingSlot, false /*wasDeleted*/, + true /*updateSlotInformation*/); + } + + newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot); + } + else + { + // We're being told to scroll beyond the last row, ignore the extra + NegVerticalOffset = 0; + break; + } + + double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); + double remainingHeight = height - deltaY; + if (MathUtilities.LessThanOrClose(rowHeight, remainingHeight)) + { + deltaY += rowHeight; + } + else + { + NegVerticalOffset = remainingHeight; + break; + } + } + } + } + } + } + else + { + // Scrolling Up + if (MathUtilities.GreaterThanOrClose(height + NegVerticalOffset, 0)) + { + // We've merely exposing more of the row we're on + NegVerticalOffset += height; + } + else + { + // Figure out what row we've scrolled up to and update the value for NegVerticalOffset + deltaY = -NegVerticalOffset; + NegVerticalOffset = 0; + // + + if (height < -2 * CellsEstimatedHeight && + (RowDetailsVisibilityMode != DataGridRowDetailsVisibilityMode.VisibleWhenSelected || + RowDetailsTemplate == null)) + { + // Very large scroll occurred. Instead of determining the exact number of scrolled off rows, + // let's estimate the number based on RowHeight. + if (newVerticalOffset == 0) + { + newFirstScrollingSlot = 0; + } + else + { + double singleRowHeightEstimate = RowHeightEstimate + + (RowDetailsVisibilityMode == + DataGridRowDetailsVisibilityMode.Visible + ? RowDetailsHeightEstimate + : 0); + int scrolledToSlot = newFirstScrollingSlot + (int)(height / singleRowHeightEstimate); + scrolledToSlot -= _collapsedSlotsTable.GetIndexCount(scrolledToSlot, newFirstScrollingSlot); + + newFirstScrollingSlot = Math.Max(0, GetPreviousVisibleSlot(scrolledToSlot + 1)); + } + + ResetDisplayedRows(); + } + else + { + int lastScrollingSlot = DisplayData.LastScrollingSlot; + while (MathUtilities.GreaterThan(deltaY, height)) + { + if (newFirstScrollingSlot > 0) + { + if (IsSlotVisible(lastScrollingSlot)) + { + // Make the bottom row available for reuse + RemoveDisplayedElement(lastScrollingSlot, wasDeleted: false, + updateSlotInformation: true); + lastScrollingSlot = GetPreviousVisibleSlot(lastScrollingSlot); + } + + newFirstScrollingSlot = GetPreviousVisibleSlot(newFirstScrollingSlot); + } + else + { + NegVerticalOffset = 0; + break; + } + + double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); + double remainingHeight = height - deltaY; + if (MathUtilities.LessThanOrClose(rowHeight + remainingHeight, 0)) + { + deltaY -= rowHeight; + } + else + { + NegVerticalOffset = rowHeight + remainingHeight; + break; + } + } + } + } + + if (MathUtilities.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) + { + // We've scrolled to the top of the ScrollBar, automatically place the user at the very top + // of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate. + // strategy. For most data, this should be unnoticeable. + ResetDisplayedRows(); + NegVerticalOffset = 0; + UpdateDisplayedRows(0, CellsEstimatedHeight); + newFirstScrollingSlot = 0; + } + } + + double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); + if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset)) + { + // We've scrolled off more of the first row than what's possible. This can happen + // if the first row got shorter (Ex: Collapsing RowDetails) or if the user has a recycling + // cleanup issue. In this case, simply try to display the next row as the first row instead + if (newFirstScrollingSlot < SlotCount - 1) + { + newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot); + Debug.Assert(newFirstScrollingSlot != -1); + } + + NegVerticalOffset = 0; + } + + UpdateDisplayedRows(newFirstScrollingSlot, CellsEstimatedHeight); + + double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot); + if (MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight)) + { + int firstElementSlot = DisplayData.FirstScrollingSlot; + // We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element + while (newFirstScrollingSlot > 0 && MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight)) + { + int previousSlot = GetPreviousVisibleSlot(firstElementSlot); + if (previousSlot == -1) + { + NegVerticalOffset = 0; + _verticalOffset = 0; + } + else + { + NegVerticalOffset -= firstElementHeight; + _verticalOffset = Math.Max(0, _verticalOffset - firstElementHeight); + firstElementSlot = previousSlot; + firstElementHeight = GetExactSlotElementHeight(firstElementSlot); + } + } + + // We could be smarter about this, but it's not common so we wouldn't gain much from optimizing here + if (firstElementSlot != DisplayData.FirstScrollingSlot) + { + UpdateDisplayedRows(firstElementSlot, CellsEstimatedHeight); + } + } + + Debug.Assert(DisplayData.FirstScrollingSlot >= 0); + Debug.Assert(GetExactSlotElementHeight(DisplayData.FirstScrollingSlot) > NegVerticalOffset); + + if (DisplayData.FirstScrollingSlot == 0) + { + _verticalOffset = NegVerticalOffset; + } + else if (MathUtilities.GreaterThan(NegVerticalOffset, newVerticalOffset)) + { + // The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb + // can stay in the same place + NegVerticalOffset = newVerticalOffset; + _verticalOffset = newVerticalOffset; + } + else + { + _verticalOffset = newVerticalOffset; + } + + Debug.Assert(!(_verticalOffset == 0 && NegVerticalOffset == 0 && DisplayData.FirstScrollingSlot > 0)); + + SetVerticalOffset(_verticalOffset); + + DisplayData.FullyRecycleElements(); + + Debug.Assert(MathUtilities.GreaterThanOrClose(NegVerticalOffset, 0)); + Debug.Assert(MathUtilities.GreaterThanOrClose(_verticalOffset, NegVerticalOffset)); + } + finally + { + _scrollingByHeight = false; + } + } + + private void SelectDisplayedElement(int slot) + { + Debug.Assert(IsSlotVisible(slot)); + Control element = DisplayData.GetDisplayedElement(slot); + if (element is DataGridRow row) + { + row.ApplyState(); + EnsureRowDetailsVisibility(row, raiseNotification: true, animate: true); + } + else + { + // Assume it's a RowGroupHeader + DataGridRowGroupHeader? groupHeader = element as DataGridRowGroupHeader; + groupHeader?.UpdatePseudoClasses(); + } + } + + private void SelectSlot(int slot, bool isSelected) + { + _selectedItems.SelectSlot(slot, isSelected); + if (IsSlotVisible(slot)) + { + SelectDisplayedElement(slot); + } + } + + private void SelectSlots(int startSlot, int endSlot, bool isSelected) + { + _selectedItems.SelectSlots(startSlot, endSlot, isSelected); + + // Apply the correct row state for display rows and also expand or collapse detail accordingly + int firstSlot = Math.Max(DisplayData.FirstScrollingSlot, startSlot); + int lastSlot = Math.Min(DisplayData.LastScrollingSlot, endSlot); + + for (int slot = firstSlot; slot <= lastSlot; slot++) + { + if (IsSlotVisible(slot)) + { + SelectDisplayedElement(slot); + } + } + } + + private void UnloadElements(bool recycle) + { + // Since we're unloading all the elements, we can't be in editing mode anymore, + // so commit if we can, otherwise force cancel. + if (!CommitEdit()) + { + CancelEdit(DataGridEditingUnit.Row, false); + } + + ResetEditingRow(); + + // Make sure to clear the focused row (because it's no longer relevant). + if (_focusedRow != null) + { + ResetFocusedRow(); + Focus(); + } + + if (_rowsPresenter != null) + { + foreach (Control element in _rowsPresenter.Children) + { + if (element is DataGridRow row) + { + // Raise UnloadingRow for any row that was visible + if (IsSlotVisible(row.Slot)) + { + OnUnloadingRow(new DataGridRowEventArgs(row)); + } + + row.DetachFromDataGrid(recycle && row.IsRecyclable /*recycle*/); + } + else if (element is DataGridRowGroupHeader groupHeader) + { + if (groupHeader.RowGroupInfo != null && IsSlotVisible(groupHeader.RowGroupInfo.Slot)) + { + OnUnloadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader)); + } + } + } + + if (!recycle) + { + _rowsPresenter.Children.Clear(); + } + } + + DisplayData.ClearElements(recycle); + + // Update the AvailableRowRoom since we're displaying 0 rows now + AvailableSlotElementRoom = CellsEstimatedHeight; + VisibleSlotCount = 0; + } + + private void UnloadRow(DataGridRow dataGridRow) + { + Debug.Assert(dataGridRow != null); + Debug.Assert(_rowsPresenter != null); + Debug.Assert(_rowsPresenter.Children.Contains(dataGridRow)); + + if (_loadedRows.Contains(dataGridRow)) + { + return; // The row is still referenced, we can't release it. + } + + // Raise UnloadingRow regardless of whether the row will be recycled + OnUnloadingRow(new DataGridRowEventArgs(dataGridRow)); + bool recycleRow = CurrentSlot != dataGridRow.Index; + + if (recycleRow) + { + DisplayData.AddRecyclableRow(dataGridRow); + } + else + { + // + _rowsPresenter.Children.Remove(dataGridRow); + dataGridRow.DetachFromDataGrid(false); + } + } + + private void UpdateDisplayedRows(int newFirstDisplayedSlot, double displayHeight) + { + Debug.Assert(!_collapsedSlotsTable.Contains(newFirstDisplayedSlot)); + int firstDisplayedScrollingSlot = newFirstDisplayedSlot; + int lastDisplayedScrollingSlot = -1; + double deltaY = -NegVerticalOffset; + int visibleScrollingRows = 0; + + if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) + { + return; + } + + if (firstDisplayedScrollingSlot == -1) + { + // 0 is fine because the element in the first slot cannot be collapsed + firstDisplayedScrollingSlot = 0; + } + + int slot = firstDisplayedScrollingSlot; + while (slot < SlotCount && !MathUtilities.GreaterThanOrClose(deltaY, displayHeight)) + { + deltaY += GetExactSlotElementHeight(slot); + visibleScrollingRows++; + lastDisplayedScrollingSlot = slot; + slot = GetNextVisibleSlot(slot); + } + + while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0) + { + slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot); + if (slot >= 0) + { + deltaY += GetExactSlotElementHeight(slot); + firstDisplayedScrollingSlot = slot; + visibleScrollingRows++; + } + } + + // If we're up to the first row, and we still have room left, uncover as much of the first row as we can + if (firstDisplayedScrollingSlot == 0 && MathUtilities.LessThan(deltaY, displayHeight)) + { + double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY); + deltaY += NegVerticalOffset - newNegVerticalOffset; + NegVerticalOffset = newNegVerticalOffset; + } + + if (MathUtilities.GreaterThan(deltaY, displayHeight) || (MathUtilities.AreClose(deltaY, displayHeight) && + MathUtilities.GreaterThan(NegVerticalOffset, 0))) + { + DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1; + } + else + { + DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows; + } + + if (visibleScrollingRows == 0) + { + firstDisplayedScrollingSlot = -1; + Debug.Assert(lastDisplayedScrollingSlot == -1); + } + + Debug.Assert(lastDisplayedScrollingSlot < SlotCount, "lastDisplayedScrollingRow larger than number of rows"); + + RemoveNonDisplayedRows(firstDisplayedScrollingSlot, lastDisplayedScrollingSlot); + + Debug.Assert(DisplayData.NumDisplayedScrollingElements >= 0, + "the number of visible scrolling rows can't be negative"); + Debug.Assert(DisplayData.NumTotallyDisplayedScrollingElements >= 0, + "the number of totally visible scrolling rows can't be negative"); + Debug.Assert(DisplayData.FirstScrollingSlot < SlotCount, + "firstDisplayedScrollingRow larger than number of rows"); + Debug.Assert(DisplayData.FirstScrollingSlot == firstDisplayedScrollingSlot); + Debug.Assert(DisplayData.LastScrollingSlot == lastDisplayedScrollingSlot); + } + + // Similar to UpdateDisplayedRows except that it starts with the LastDisplayedScrollingRow + // and computes the FirstDisplayScrollingRow instead of doing it the other way around. We use this + // when scrolling down to a full row + private void UpdateDisplayedRowsFromBottom(int newLastDisplayedScrollingRow) + { + //Debug.Assert(!_collapsedSlotsTable.Contains(newLastDisplayedScrollingRow)); + + int lastDisplayedScrollingRow = newLastDisplayedScrollingRow; + int firstDisplayedScrollingRow = -1; + double displayHeight = CellsEstimatedHeight; + double deltaY = 0; + int visibleScrollingRows = 0; + + if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) + { + ResetDisplayedRows(); + return; + } + + if (lastDisplayedScrollingRow == -1) + { + lastDisplayedScrollingRow = 0; + } + + int slot = lastDisplayedScrollingRow; + while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0) + { + deltaY += GetExactSlotElementHeight(slot); + visibleScrollingRows++; + firstDisplayedScrollingRow = slot; + slot = GetPreviousVisibleSlot(slot); + } + + DisplayData.NumTotallyDisplayedScrollingElements = + deltaY > displayHeight ? visibleScrollingRows - 1 : visibleScrollingRows; + + Debug.Assert(DisplayData.NumTotallyDisplayedScrollingElements >= 0); + Debug.Assert(lastDisplayedScrollingRow < SlotCount, "lastDisplayedScrollingRow larger than number of rows"); + + NegVerticalOffset = Math.Max(0, deltaY - displayHeight); + + RemoveNonDisplayedRows(firstDisplayedScrollingRow, lastDisplayedScrollingRow); + + Debug.Assert(DisplayData.NumDisplayedScrollingElements >= 0, + "the number of visible scrolling rows can't be negative"); + Debug.Assert(DisplayData.NumTotallyDisplayedScrollingElements >= 0, + "the number of totally visible scrolling rows can't be negative"); + Debug.Assert(DisplayData.FirstScrollingSlot < SlotCount, + "firstDisplayedScrollingRow larger than number of rows"); + } + + private void UpdateTablesForRemoval(int slotDeleted, object? itemDeleted) + { + if (RowGroupHeadersTable.Contains(slotDeleted)) + { + // A RowGroupHeader was removed + RowGroupHeadersTable.RemoveIndexAndValue(slotDeleted); + _collapsedSlotsTable.RemoveIndexAndValue(slotDeleted); + _selectedItems.DeleteSlot(slotDeleted); + } + else + { + // Update the ranges of selected rows + if (_selectedItems.ContainsSlot(slotDeleted)) + { + SelectionHasChanged = true; + } + + _selectedItems.Delete(slotDeleted, itemDeleted); + RowGroupHeadersTable.RemoveIndex(slotDeleted); + _collapsedSlotsTable.RemoveIndex(slotDeleted); + } + } + + private void HandleCollectionViewGroupCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // If we receive this event when the number of GroupDescriptions is different than what we have already + // accounted for, that means the ICollectionView is still in the process of updating its groups. It will + // send a reset notification when it's done, at which point we can update our visuals. + + if (DataConnection.CollectionView != null && + DataConnection.CollectionView.IsGrouping && + DataConnection.CollectionView.GroupingDepth == _rowGroupHeightsByLevel.Length) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + HandleCollectionViewGroupCollectionChangedAdd(sender, e); + break; + case NotifyCollectionChangedAction.Remove: + HandleCollectionViewGroupCollectionChangedRemove(sender, e); + break; + } + } + } + + private void HandleCollectionViewGroupCollectionChangedAdd(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.NewItems != null && e.NewItems.Count > 0) + { + // We need to figure out the CollectionViewGroup that the sender belongs to. We could cache + // it by tagging the collections ahead of time, but I think the extra storage might not be worth + // it since this lookup should be performant enough + int insertSlot = -1; + DataGridRowGroupInfo? parentGroupInfo = GetParentGroupInfo(sender); + DataGridCollectionViewGroup? group = e.NewItems[0] as DataGridCollectionViewGroup; + + if (parentGroupInfo != null) + { + if (group != null || parentGroupInfo.Level == -1) + { + insertSlot = parentGroupInfo.Slot + 1; + // For groups, we need to skip over subgroups to find the correct slot + DataGridRowGroupInfo? groupInfo; + for (int i = 0; i < e.NewStartingIndex; i++) + { + do + { + insertSlot = RowGroupHeadersTable.GetNextIndex(insertSlot); + groupInfo = RowGroupHeadersTable.GetValueAt(insertSlot); + } while (groupInfo != null && groupInfo.Level > parentGroupInfo.Level + 1); + + if (groupInfo == null) + { + // We couldn't find the subchild so this should go at the end + insertSlot = SlotCount; + } + } + } + else + { + // For items the slot is a simple calculation + insertSlot = parentGroupInfo.Slot + e.NewStartingIndex + 1; + } + } + + // This could not be found when new GroupDescriptions are added to the PagedCollectionView + if (insertSlot != -1) + { + bool isCollapsed = (parentGroupInfo != null) && + (!parentGroupInfo.IsVisible || _collapsedSlotsTable.Contains(parentGroupInfo.Slot)); + if (group != null) + { + Debug.Assert(parentGroupInfo != null); + group.Items.CollectionChanged += HandleCollectionViewGroupCollectionChanged; + + var newGroupInfo = + new DataGridRowGroupInfo(group, true, parentGroupInfo.Level + 1, insertSlot, insertSlot); + InsertElementAt(insertSlot, + rowIndex: -1, + item: null, + groupInfo: newGroupInfo, + isCollapsed: isCollapsed); + RowGroupHeadersTable.AddValue(insertSlot, newGroupInfo); + } + else + { + // Assume we're adding a new row + int rowIndex = DataConnection.IndexOf(e.NewItems[0]); + Debug.Assert(rowIndex != -1); + if (SlotCount == 0 && DataConnection.ShouldAutoGenerateColumns) + { + AutoGenerateColumnsPrivate(); + } + + InsertElementAt(insertSlot, rowIndex, + item: e.NewItems[0], + groupInfo: null, + isCollapsed: isCollapsed); + } + + CorrectLastSubItemSlotsAfterInsertion(parentGroupInfo); + if (parentGroupInfo != null && (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1)) + { + // We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed + EnsureAncestorsExpanderButtonChecked(parentGroupInfo); + } + } + } + } + + private void HandleCollectionViewGroupCollectionChangedRemove(object? sender, NotifyCollectionChangedEventArgs e) + { + Debug.Assert(e.OldItems != null); + Debug.Assert(e.OldItems.Count == 1); + if (e.OldItems != null && e.OldItems.Count > 0) + { + if (e.OldItems[0] is DataGridCollectionViewGroup removedGroup) + { + removedGroup.Items.CollectionChanged -= HandleCollectionViewGroupCollectionChanged; + + DataGridRowGroupInfo? groupInfo = RowGroupInfoFromCollectionViewGroup(removedGroup); + Debug.Assert(groupInfo != null); + if ((groupInfo.Level == _rowGroupHeightsByLevel.Length - 1) && + (removedGroup.Items.Count > 0)) + { + Debug.Assert((groupInfo.LastSubItemSlot - groupInfo.Slot) == removedGroup.Items.Count); + // If we're removing a leaf Group then remove all of its items before removing the Group + for (int i = 0; i < removedGroup.Items.Count; i++) + { + RemoveElementAt(groupInfo.Slot + 1, item: removedGroup.Items[i], isRow: true); + } + } + + RemoveElementAt(groupInfo.Slot, item: null, isRow: false); + } + else + { + // A single item was removed from a leaf group + DataGridRowGroupInfo? parentGroupInfo = GetParentGroupInfo(sender); + if (parentGroupInfo != null) + { + int slot; + if (RowGroupHeadersTable.IndexCount > 0) + { + // In this case, we're removing from the root group. If there are other groups, then this must + // be the new item row that doesn't belong to any group because if there are other groups then + // this item cannot be a child of the root group. + slot = SlotCount - 1; + } + else + { + slot = parentGroupInfo.Slot + e.OldStartingIndex + 1; + } + + RemoveElementAt(slot, e.OldItems[0], isRow: true); + } + } + } + } + + private void ClearRowGroupHeadersTable() + { + // Detach existing handlers on CollectionViewGroup.Items.CollectionChanged + foreach (int slot in RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo? groupInfo = RowGroupHeadersTable.GetValueAt(slot); + if (groupInfo?.CollectionViewGroup != null) + { + groupInfo.CollectionViewGroup.Items.CollectionChanged -= HandleCollectionViewGroupCollectionChanged; + } + } + + if (_topLevelGroup != null) + { + // The PagedCollectionView reuses the top level group so we need to detach any existing or else we'll get duplicate handers here + _topLevelGroup.CollectionChanged -= HandleCollectionViewGroupCollectionChanged; + _topLevelGroup = null; + } + + RowGroupHeadersTable.Clear(); + // Unfortunately PagedCollectionView does not allow us to preserve expanded or collapsed states for RowGroups since + // the CollectionViewGroups are recreated when a Reset happens. This is true in both SL and WPF + _collapsedSlotsTable.Clear(); + + _rowGroupHeightsByLevel = []; + RowGroupSublevelIndents = []; + } + + private void HandleCollectionViewGroupPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "ItemCount") + { + DataGridRowGroupInfo? rowGroupInfo = + RowGroupInfoFromCollectionViewGroup(sender as DataGridCollectionViewGroup); + if (rowGroupInfo != null && IsSlotVisible(rowGroupInfo.Slot)) + { + if (DisplayData.GetDisplayedElement(rowGroupInfo.Slot) is DataGridRowGroupHeader rowGroupHeader) + { + rowGroupHeader.UpdateTitleElements(); + } + } + } + } + + // This method is necessary for incrementing the LastSubItemSlot property of the group ancestors + // because CorrectSlotsAfterInsertion only increments those that come after the specified group + private void CorrectLastSubItemSlotsAfterInsertion(DataGridRowGroupInfo? subGroupInfo) + { + int subGroupSlot; + int subGroupLevel; + while (subGroupInfo != null) + { + subGroupLevel = subGroupInfo.Level; + subGroupInfo.LastSubItemSlot++; + + while (subGroupInfo != null && subGroupInfo.Level >= subGroupLevel) + { + subGroupSlot = RowGroupHeadersTable.GetPreviousIndex(subGroupInfo.Slot); + subGroupInfo = RowGroupHeadersTable.GetValueAt(subGroupSlot); + } + } + } + + private int CountAndPopulateGroupHeaders(object group, int rootSlot, int level) + { + int treeCount = 1; + + if (group is DataGridCollectionViewGroup collectionViewGroup) + { + if (collectionViewGroup.Items.Count > 0) + { + collectionViewGroup.Items.CollectionChanged += HandleCollectionViewGroupCollectionChanged; + if (collectionViewGroup.Items[0] is DataGridCollectionViewGroup) + { + foreach (object subGroup in collectionViewGroup.Items) + { + treeCount += CountAndPopulateGroupHeaders(subGroup, rootSlot + treeCount, level + 1); + } + } + else + { + // Optimization: don't walk to the bottom level nodes + treeCount += collectionViewGroup.Items.Count; + } + } + + RowGroupHeadersTable.AddValue(rootSlot, + new DataGridRowGroupInfo(collectionViewGroup, true, level, rootSlot, rootSlot + treeCount - 1)); + } + + return treeCount; + } + + private void EnsureAncestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) + { + if (IsSlotVisible(parentGroupInfo.Slot)) + { + DataGridRowGroupHeader? ancestorGroupHeader = + DisplayData.GetDisplayedElement(parentGroupInfo.Slot) as DataGridRowGroupHeader; + while (ancestorGroupHeader != null) + { + ancestorGroupHeader.EnsureExpanderButtonIsChecked(); + if (ancestorGroupHeader.Level > 0) + { + Debug.Assert(ancestorGroupHeader.RowGroupInfo != null); + int slot = RowGroupHeadersTable.GetPreviousIndex(ancestorGroupHeader.RowGroupInfo.Slot); + if (IsSlotVisible(slot)) + { + ancestorGroupHeader = DisplayData.GetDisplayedElement(slot) as DataGridRowGroupHeader; + continue; + } + } + + break; + } + } + } + + private void PopulateRowGroupHeadersTable() + { + if (DataConnection.CollectionView != null + && DataConnection.CollectionView.CanGroup + && DataConnection.CollectionView.Groups != null) + { + int totalSlots = 0; + _topLevelGroup = (INotifyCollectionChanged)DataConnection.CollectionView.Groups; + _topLevelGroup.CollectionChanged += HandleCollectionViewGroupCollectionChanged; + foreach (object group in DataConnection.CollectionView.Groups) + { + totalSlots += CountAndPopulateGroupHeaders(group, totalSlots, 0); + } + } + + SlotCount = DataConnection.Count + RowGroupHeadersTable.IndexCount; + VisibleSlotCount = SlotCount; + } + + private void RefreshRowGroupHeaders() + { + if (DataConnection.CollectionView != null + && DataConnection.CollectionView.CanGroup + && DataConnection.CollectionView.Groups != null + && DataConnection.CollectionView.IsGrouping + && DataConnection.CollectionView.GroupingDepth > 0) + { + // Initialize our array for the height of the RowGroupHeaders by Level. + // If the Length is the same, we can reuse the old array + int groupLevelCount = DataConnection.CollectionView.GroupingDepth; + if (_rowGroupHeightsByLevel.Length == 0 || _rowGroupHeightsByLevel.Length != groupLevelCount) + { + _rowGroupHeightsByLevel = new double[groupLevelCount]; + for (int i = 0; i < groupLevelCount; i++) + { + // Default height for now, the actual heights are updated as the RowGroupHeaders + // are added and measured + _rowGroupHeightsByLevel[i] = DefaultRowHeight; + } + } + + if (RowGroupSublevelIndents.Length == 0 || RowGroupSublevelIndents.Length != groupLevelCount) + { + RowGroupSublevelIndents = new double[groupLevelCount]; + double indent; + for (int i = 0; i < groupLevelCount; i++) + { + indent = DefaultRowGroupSublevelIndent; + RowGroupSublevelIndents[i] = indent; + if (i > 0) + { + RowGroupSublevelIndents[i] += RowGroupSublevelIndents[i - 1]; + } + } + } + + EnsureRowGroupSpacerColumnWidth(groupLevelCount); + } + } + + private void EnsureRowGroupSpacerColumn() + { + bool spacerColumnChanged = ColumnsInternal.EnsureRowGrouping(!RowGroupHeadersTable.IsEmpty); + if (spacerColumnChanged) + { + if (ColumnsInternal.RowGroupSpacerColumn.IsRepresented && CurrentColumnIndex == 0) + { + CurrentColumn = ColumnsInternal.FirstVisibleNonFillerColumn; + } + + ProcessFrozenColumnCount(); + } + } + + private void EnsureRowGroupSpacerColumnWidth(int groupLevelCount) + { + if (groupLevelCount == 0) + { + ColumnsInternal.RowGroupSpacerColumn.Width = new DataGridLength(0); + } + else + { + ColumnsInternal.RowGroupSpacerColumn.Width = + new DataGridLength(RowGroupSublevelIndents[groupLevelCount - 1]); + } + } + + private void EnsureRowGroupVisibility(DataGridRowGroupInfo? rowGroupInfo, bool isVisible, bool setCurrent) + { + if (rowGroupInfo == null) + { + return; + } + + if (rowGroupInfo.IsVisible != isVisible) + { + if (IsSlotVisible(rowGroupInfo.Slot)) + { + DataGridRowGroupHeader? rowGroupHeader = + DisplayData.GetDisplayedElement(rowGroupInfo.Slot) as DataGridRowGroupHeader; + Debug.Assert(rowGroupHeader != null); + rowGroupHeader.ToggleExpandCollapse(isVisible, setCurrent); + } + else + { + if (_collapsedSlotsTable.Contains(rowGroupInfo.Slot)) + { + // Somewhere up the parent chain, there's a collapsed header so all the slots remain the same and + // we just need to mark this header with the new visibility + rowGroupInfo.IsVisible = isVisible; + } + else + { + if (rowGroupInfo.Slot < DisplayData.FirstScrollingSlot) + { + double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false); + // Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative + // the user can still scroll to the top since the offset is non-zero + SetVerticalOffset(Math.Max(MathUtils.DoubleEpsilon, _verticalOffset + heightChange)); + } + else + { + UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false); + } + + UpdateVerticalScrollBar(); + } + } + } + } + + // Returns the inclusive count of expanded RowGroupHeaders from startSlot to endSlot + private int GetRowGroupHeaderCount(int startSlot, int endSlot, bool? isVisible, out double headersHeight) + { + int count = 0; + headersHeight = 0; + foreach (int slot in RowGroupHeadersTable.GetIndexes(startSlot)) + { + if (slot > endSlot) + { + return count; + } + + DataGridRowGroupInfo? rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot); + if (!isVisible.HasValue || + (isVisible.Value && !_collapsedSlotsTable.Contains(slot)) || + (!isVisible.Value && _collapsedSlotsTable.Contains(slot))) + { + Debug.Assert(rowGroupInfo != null); + count++; + headersHeight += _rowGroupHeightsByLevel[rowGroupInfo.Level]; + } + } + + return count; + } + + // This method does not check the state of the parent RowGroupHeaders, it assumes they're ready for this newVisibility to + // be applied this header + // Returns the number of pixels that were expanded or (collapsed); however, if we're expanding displayed rows, we only expand up + // to what we can display + private double UpdateRowGroupVisibility(DataGridRowGroupInfo targetRowGroupInfo, bool newIsVisible, + bool isDisplayed) + { + double heightChange = 0; + int slotsExpanded = 0; + int startSlot = targetRowGroupInfo.Slot + 1; + int endSlot; + + targetRowGroupInfo.IsVisible = newIsVisible; + if (newIsVisible) + { + // Expand + foreach (int slot in RowGroupHeadersTable.GetIndexes(targetRowGroupInfo.Slot + 1)) + { + if (slot >= startSlot) + { + DataGridRowGroupInfo? rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot); + Debug.Assert(rowGroupInfo != null); + if (rowGroupInfo.Level <= targetRowGroupInfo.Level) + { + break; + } + + if (!rowGroupInfo.IsVisible) + { + // Skip over the items in collapsed subgroups + endSlot = rowGroupInfo.Slot; + ExpandSlots(startSlot, endSlot, isDisplayed, ref slotsExpanded, ref heightChange); + startSlot = rowGroupInfo.LastSubItemSlot + 1; + } + } + } + + if (targetRowGroupInfo.LastSubItemSlot >= startSlot) + { + ExpandSlots(startSlot, targetRowGroupInfo.LastSubItemSlot, isDisplayed, ref slotsExpanded, + ref heightChange); + } + + if (isDisplayed) + { + UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsEstimatedHeight); + } + } + else + { + // Collapse + endSlot = SlotCount - 1; + foreach (int slot in RowGroupHeadersTable.GetIndexes(targetRowGroupInfo.Slot + 1)) + { + DataGridRowGroupInfo? rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot); + Debug.Assert(rowGroupInfo != null); + if (rowGroupInfo.Level <= targetRowGroupInfo.Level) + { + endSlot = slot - 1; + break; + } + } + + int oldLastDisplayedSlot = DisplayData.LastScrollingSlot; + int endDisplayedSlot = Math.Min(endSlot, DisplayData.LastScrollingSlot); + if (isDisplayed) + { + // We need to remove all the displayed slots that aren't already collapsed + int elementsToRemove = endDisplayedSlot - startSlot + 1 - + _collapsedSlotsTable.GetIndexCount(startSlot, endDisplayedSlot); + + if (_focusedRow != null && _focusedRow.Slot >= startSlot && _focusedRow.Slot <= endSlot) + { + Debug.Assert(EditingRow == null); + // Don't call ResetFocusedRow here because we're already cleaning it up below, and we don't want to FullyRecycle yet + _focusedRow = null; + } + + for (int i = 0; i < elementsToRemove; i++) + { + RemoveDisplayedElement(startSlot, wasDeleted: false, updateSlotInformation: false); + } + } + + double heightChangeBelowLastDisplayedSlot = 0; + if (DisplayData.FirstScrollingSlot >= startSlot && DisplayData.FirstScrollingSlot <= endSlot) + { + // Our first visible slot was collapsed, find the replacement + int collapsedSlotsAbove = DisplayData.FirstScrollingSlot - startSlot - + _collapsedSlotsTable.GetIndexCount(startSlot, DisplayData.FirstScrollingSlot); + Debug.Assert(collapsedSlotsAbove > 0); + int newFirstScrollingSlot = GetNextVisibleSlot(DisplayData.FirstScrollingSlot); + while (collapsedSlotsAbove > 1 && newFirstScrollingSlot < SlotCount) + { + collapsedSlotsAbove--; + newFirstScrollingSlot = GetNextVisibleSlot(newFirstScrollingSlot); + } + + heightChange += CollapseSlotsInTable(startSlot, endSlot, ref slotsExpanded, oldLastDisplayedSlot, + ref heightChangeBelowLastDisplayedSlot); + if (isDisplayed) + { + if (newFirstScrollingSlot >= SlotCount) + { + // No visible slots below, look up + UpdateDisplayedRowsFromBottom(targetRowGroupInfo.Slot); + } + else + { + UpdateDisplayedRows(newFirstScrollingSlot, CellsEstimatedHeight); + } + } + } + else + { + heightChange += CollapseSlotsInTable(startSlot, endSlot, ref slotsExpanded, oldLastDisplayedSlot, + ref heightChangeBelowLastDisplayedSlot); + } + + if (DisplayData.LastScrollingSlot >= startSlot && DisplayData.LastScrollingSlot <= endSlot) + { + // Collapsed the last scrolling row, we need to update it + DisplayData.LastScrollingSlot = GetPreviousVisibleSlot(DisplayData.LastScrollingSlot); + } + + // Collapsing could cause the vertical offset to move up if we collapsed a lot of slots + // near the bottom of the DataGrid. To do this, we compare the height we collapsed to + // the distance to the last visible row and adjust the scrollbar if we collapsed more + if (isDisplayed && _verticalOffset > 0) + { + int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount); + int slot = GetNextVisibleSlot(oldLastDisplayedSlot); + // AvailableSlotElementRoom ends up being the amount of the last slot that is partially scrolled off + // as a negative value, heightChangeBelowLastDisplayed slot is also a negative value since we're collapsing + double heightToLastVisibleSlot = AvailableSlotElementRoom + heightChangeBelowLastDisplayedSlot; + while ((heightToLastVisibleSlot > heightChange) && (slot < lastVisibleSlot)) + { + heightToLastVisibleSlot -= GetSlotElementHeight(slot); + slot = GetNextVisibleSlot(slot); + } + + if (heightToLastVisibleSlot > heightChange) + { + double newVerticalOffset = _verticalOffset + heightChange - heightToLastVisibleSlot; + if (newVerticalOffset > 0) + { + SetVerticalOffset(newVerticalOffset); + } + else + { + // Collapsing causes the vertical offset to go to 0 so we should go back to the first row. + ResetDisplayedRows(); + NegVerticalOffset = 0; + SetVerticalOffset(0); + int firstDisplayedRow = GetNextVisibleSlot(-1); + UpdateDisplayedRows(firstDisplayedRow, CellsEstimatedHeight); + } + } + } + } + + // Update VisibleSlotCount + VisibleSlotCount += slotsExpanded; + + return heightChange; + } + + private DataGridRowGroupHeader GenerateRowGroupHeader(int slot, DataGridRowGroupInfo? rowGroupInfo) + { + Debug.Assert(slot > -1); + Debug.Assert(rowGroupInfo != null); + + DataGridRowGroupHeader groupHeader = DisplayData.GetUsedGroupHeader() ?? new DataGridRowGroupHeader(); + groupHeader.OwningGrid = this; + groupHeader.RowGroupInfo = rowGroupInfo; + groupHeader.DataContext = rowGroupInfo.CollectionViewGroup; + groupHeader.Level = rowGroupInfo.Level; + if (RowGroupTheme is { } rowGroupTheme) + { + groupHeader.SetValue(ThemeProperty, rowGroupTheme, BindingPriority.Template); + } + + // Set the RowGroupHeader's PropertyName. Unfortunately, CollectionViewGroup doesn't have this + // so we have to set it manually + Debug.Assert(DataConnection.CollectionView != null && + groupHeader.Level < DataConnection.CollectionView.GroupingDepth); + string propertyName = DataConnection.CollectionView.GetGroupingPropertyNameAtDepth(groupHeader.Level); + + if (string.IsNullOrWhiteSpace(propertyName)) + { + groupHeader.PropertyName = null; + } + else + { + groupHeader.PropertyName = DataConnection.DataType?.GetDisplayName(propertyName) ?? propertyName; + } + + if (rowGroupInfo.CollectionViewGroup is INotifyPropertyChanged inpc) + { + inpc.PropertyChanged -= new PropertyChangedEventHandler(HandleCollectionViewGroupPropertyChanged); + inpc.PropertyChanged += new PropertyChangedEventHandler(HandleCollectionViewGroupPropertyChanged); + } + + groupHeader.UpdateTitleElements(); + + OnLoadingRowGroup(new DataGridRowGroupHeaderEventArgs(groupHeader)); + + return groupHeader; + } + + private DataGridRowGroupInfo? GetParentGroupInfo(object? collection) + { + if (collection == DataConnection.CollectionView?.Groups) + { + // If the new item is a root level element, it has no parent group, so create an empty RowGroupInfo + return new DataGridRowGroupInfo(null, true, -1, -1, -1); + } + foreach (int slot in RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo? groupInfo = RowGroupHeadersTable.GetValueAt(slot); + if (groupInfo?.CollectionViewGroup?.Items == collection) + { + return groupInfo; + } + } + + return null; + } + + internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisible, bool setCurrent) + { + Debug.Assert(groupHeader.RowGroupInfo?.CollectionViewGroup != null && groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0); + + if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisible, setCurrent); }) || + !CommitEdit()) + { + return; + } + + if (setCurrent && CurrentSlot != groupHeader.RowGroupInfo.Slot) + { + // Most of the time this is set by the MouseLeftButtonDown handler but validation could cause that code path to fail + UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, + DataGridSelectionAction.SelectCurrent, scrollIntoView: false); + } + + UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisible, isDisplayed: true); + + ComputeScrollBarsLayout(); + // We need force arrange since our Scrollings Rows could update without automatically triggering layout + InvalidateRowsArrange(); + } + + internal void OnSublevelIndentUpdated(DataGridRowGroupHeader groupHeader, double newValue) + { + Debug.Assert(DataConnection.CollectionView != null); + Debug.Assert(RowGroupSublevelIndents != null); + + int groupLevelCount = DataConnection.CollectionView.GroupingDepth; + Debug.Assert(groupHeader.Level >= 0 && groupHeader.Level < groupLevelCount); + + double oldValue = RowGroupSublevelIndents[groupHeader.Level]; + if (groupHeader.Level > 0) + { + oldValue -= RowGroupSublevelIndents[groupHeader.Level - 1]; + } + + // Update the affected values in our table by the amount affected + double change = newValue - oldValue; + for (int i = groupHeader.Level; i < groupLevelCount; i++) + { + RowGroupSublevelIndents[i] += change; + Debug.Assert(RowGroupSublevelIndents[i] >= 0); + } + + EnsureRowGroupSpacerColumnWidth(groupLevelCount); + } + + internal DataGridRowGroupInfo? RowGroupInfoFromCollectionViewGroup(DataGridCollectionViewGroup? collectionViewGroup) + { + foreach (int slot in RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo? rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot); + if (rowGroupInfo?.CollectionViewGroup == collectionViewGroup) + { + return rowGroupInfo; + } + } + + return null; + } + + /// + /// Collapses the DataGridRowGroupHeader that represents a given CollectionViewGroup + /// + /// CollectionViewGroup + /// Set to true to collapse all Subgroups + public void CollapseRowGroup(DataGridCollectionViewGroup? collectionViewGroup, bool collapseAllSubgroups) + { + if (WaitForLostFocus(delegate { CollapseRowGroup(collectionViewGroup, collapseAllSubgroups); }) || + collectionViewGroup == null || !CommitEdit()) + { + return; + } + + EnsureRowGroupVisibility(RowGroupInfoFromCollectionViewGroup(collectionViewGroup), false, true); + + if (collapseAllSubgroups) + { + foreach (object groupObj in collectionViewGroup.Items) + { + if (groupObj is DataGridCollectionViewGroup subGroup) + { + CollapseRowGroup(subGroup, collapseAllSubgroups); + } + } + } + } + + /// + /// Expands the DataGridRowGroupHeader that represents a given CollectionViewGroup + /// + /// CollectionViewGroup + /// Set to true to expand all Subgroups + public void ExpandRowGroup(DataGridCollectionViewGroup? collectionViewGroup, bool expandAllSubgroups) + { + if (WaitForLostFocus(delegate { ExpandRowGroup(collectionViewGroup, expandAllSubgroups); }) || + collectionViewGroup == null || !CommitEdit()) + if (collectionViewGroup == null || !CommitEdit()) + { + return; + } + + EnsureRowGroupVisibility(RowGroupInfoFromCollectionViewGroup(collectionViewGroup), true, true); + + if (expandAllSubgroups) + { + foreach (object groupObj in collectionViewGroup.Items) + { + if (groupObj is DataGridCollectionViewGroup subGroup) + { + ExpandRowGroup(subGroup, expandAllSubgroups); + } + } + } + } + + // Returns the number of rows with details visible between lowerBound and upperBound exclusive. + // As of now, the caller needs to account for Collapsed slots. This method assumes everything + // is visible + private int GetDetailsCountInclusive(int lowerBound, int upperBound) + { + int indexCount = upperBound - lowerBound + 1; + if (indexCount <= 0) + { + return 0; + } + + if (RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Visible) + { + // Total rows minus ones which explicity turned details off minus the RowGroupHeaders + return indexCount - _showDetailsTable.GetIndexCount(lowerBound, upperBound, false) - + RowGroupHeadersTable.GetIndexCount(lowerBound, upperBound); + } + else if (RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.Collapsed) + { + // Total rows with details explicitly turned on + return _showDetailsTable.GetIndexCount(lowerBound, upperBound, true); + } + else if (RowDetailsVisibilityMode == DataGridRowDetailsVisibilityMode.VisibleWhenSelected) + { + // Total number of remaining rows that are selected + return _selectedItems.GetIndexCount(lowerBound, upperBound); + } + + Debug.Assert(false); // Shouldn't ever happen + return 0; + } + + private void EnsureRowDetailsVisibility(DataGridRow row, bool raiseNotification, bool animate) + { + // Show or hide RowDetails based on DataGrid settings + row.SetDetailsVisibilityInternal(GetRowDetailsVisibility(row.Index), raiseNotification, animate); + } + + private void UpdateRowDetailsHeightEstimate() + { + if (_rowsPresenter != null && _measured && RowDetailsTemplate != null) + { + object? dataItem = null; + if (VisibleSlotCount > 0) + dataItem = DataConnection.GetDataItem(0); + var detailsContent = RowDetailsTemplate.Build(dataItem); + if (detailsContent != null) + { + detailsContent.DataContext = dataItem; + _rowsPresenter.Children.Add(detailsContent); + detailsContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + RowDetailsHeightEstimate = detailsContent.DesiredSize.Height; + _rowsPresenter.Children.Remove(detailsContent); + } + } + } + + // detailsElement is the FrameworkElement created by the DetailsTemplate + internal void NotifyUnloadingRowDetails(DataGridRow row, Control detailsElement) + { + NotifyUnloadingRowDetails(new DataGridRowDetailsEventArgs(row, detailsElement)); + } + + // detailsElement is the FrameworkElement created by the DetailsTemplate + internal void NotifyLoadingRowDetails(DataGridRow row, Control detailsElement) + { + NotifyLoadingRowDetails(new DataGridRowDetailsEventArgs(row, detailsElement)); + } + + internal void NotifyRowDetailsVisibilityPropertyChanged(int rowIndex, bool isVisible) + { + Debug.Assert(rowIndex >= 0 && rowIndex < SlotCount); + + _showDetailsTable.AddValue(rowIndex, isVisible); + } + + internal bool GetRowDetailsVisibility(int rowIndex) + { + return GetRowDetailsVisibility(rowIndex, RowDetailsVisibilityMode); + } + + internal bool GetRowDetailsVisibility(int rowIndex, DataGridRowDetailsVisibilityMode gridLevelRowDetailsVisibility) + { + Debug.Assert(rowIndex != -1); + if (_showDetailsTable.Contains(rowIndex)) + { + // The user explicity set DetailsVisibility on a row so we should respect that + return _showDetailsTable.GetValueAt(rowIndex); + } + return + gridLevelRowDetailsVisibility == DataGridRowDetailsVisibilityMode.Visible || + (gridLevelRowDetailsVisibility == DataGridRowDetailsVisibilityMode.VisibleWhenSelected && + _selectedItems.ContainsSlot(SlotFromRowIndex(rowIndex))); + } + + /// + /// Raises the event. + /// + /// The event data. + protected internal virtual void OnRowDetailsVisibilityChanged(DataGridRowDetailsEventArgs e) + { + RowDetailsVisibilityChanged?.Invoke(this, e); + } + +#if DEBUG + internal void PrintRowGroupInfo() + { + Debug.WriteLine("-----------------------------------------------RowGroupHeaders"); + foreach (int slot in RowGroupHeadersTable.GetIndexes()) + { + DataGridRowGroupInfo? info = RowGroupHeadersTable.GetValueAt(slot); + Debug.WriteLine(String.Format(System.Globalization.CultureInfo.InvariantCulture, + "{0} {1} Slot:{2} Last:{3} Level:{4}", info?.CollectionViewGroup?.Key, info?.IsVisible.ToString(), slot, + info?.LastSubItemSlot, info?.Level)); + } + + Debug.WriteLine("-----------------------------------------------CollapsedSlots"); + _collapsedSlotsTable.PrintIndexes(); + } +#endif } \ No newline at end of file diff --git a/src/AtomUI.Controls.DataGrid/DataGrid.cs b/src/AtomUI.Controls.DataGrid/DataGrid.cs index 2312d8b00..3bd3544ee 100644 --- a/src/AtomUI.Controls.DataGrid/DataGrid.cs +++ b/src/AtomUI.Controls.DataGrid/DataGrid.cs @@ -4,15 +4,25 @@ // All other rights reserved. using System.Collections; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using AtomUI.Controls.Cell; using AtomUI.Controls.Data; +using AtomUI.Controls.Utils; +using AtomUI.Utils; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Styling; +using Avalonia.Utilities; +using Avalonia.VisualTree; namespace AtomUI.Controls; @@ -143,8 +153,8 @@ public partial class DataGrid : TemplatedControl public static readonly StyledProperty SelectionModeProperty = AvaloniaProperty.Register(nameof(SelectionMode)); - public static readonly StyledProperty VerticalGridLinesBrushProperty = - AvaloniaProperty.Register(nameof(VerticalGridLinesBrush)); + public static readonly StyledProperty VerticalGridLinesBrushProperty = + AvaloniaProperty.Register(nameof(VerticalGridLinesBrush)); public static readonly StyledProperty VerticalScrollBarVisibilityProperty = AvaloniaProperty.Register(nameof(VerticalScrollBarVisibility)); @@ -183,8 +193,8 @@ public partial class DataGrid : TemplatedControl public static readonly StyledProperty AreRowDetailsFrozenProperty = AvaloniaProperty.Register(nameof(AreRowDetailsFrozen)); - public static readonly StyledProperty RowDetailsTemplateProperty = - AvaloniaProperty.Register(nameof(RowDetailsTemplate)); + public static readonly StyledProperty RowDetailsTemplateProperty = + AvaloniaProperty.Register(nameof(RowDetailsTemplate)); public static readonly StyledProperty RowDetailsVisibilityModeProperty = AvaloniaProperty.Register(nameof(RowDetailsVisibilityMode)); @@ -425,7 +435,7 @@ public partial class DataGrid : TemplatedControl /// /// Gets or sets the that is used to paint grid lines separating columns. /// - public IBrush VerticalGridLinesBrush + public IBrush? VerticalGridLinesBrush { get => GetValue(VerticalGridLinesBrushProperty); set => SetValue(VerticalGridLinesBrushProperty, value); @@ -505,7 +515,7 @@ public partial class DataGrid : TemplatedControl /// /// Gets or sets the template that is used to display the content of the details section of rows. /// - public IDataTemplate RowDetailsTemplate + public IDataTemplate? RowDetailsTemplate { get => GetValue(RowDetailsTemplateProperty); set => SetValue(RowDetailsTemplateProperty, value); @@ -525,6 +535,84 @@ public partial class DataGrid : TemplatedControl /// // public IDataGridCollectionView CollectionView => // DataConnection.CollectionView; + + /// + /// Gets or sets the column that contains the current cell. + /// + 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()); + } + } + } + } + + /// + /// Gets a collection that contains all the columns in the control. + /// we use a backing field here because the field's type + /// is a subclass of the property's + /// + public ObservableCollection Columns => ColumnsInternal; + + /// + /// Gets a list that contains the data items corresponding to the selected rows. + /// + public IList SelectedItems => _selectedItems as IList; + #endregion #region 属性验证方法 @@ -679,8 +767,39 @@ public partial class DataGrid : TemplatedControl public event EventHandler? HorizontalScroll; public event EventHandler? VerticalScroll; + + /// + /// Occurs before a DataGridRowGroupHeader header is used. + /// + public event EventHandler? LoadingRowGroup; + + /// + /// Occurs when the DataGridRowGroupHeader is available for reuse. + /// + public event EventHandler? UnloadingRowGroup; + + /// + /// 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. + /// + public event EventHandler? CopyingRowClipboardContent; #endregion + + /// + /// Gets the data item bound to the row that contains the current cell. + /// + protected object? CurrentItem + { + get + { + if (CurrentSlot == -1 || ItemsSource == null || RowGroupHeadersTable.Contains(CurrentSlot)) + { + return null; + } + return DataConnection.GetDataItem(RowIndexFromSlot(CurrentSlot)); + } + } static DataGrid() { @@ -727,7 +846,4151 @@ public partial class DataGrid : TemplatedControl CurrentCellCoordinates = new DataGridCellCoordinates(-1, -1); RowGroupHeaderHeightEstimate = DefaultRowHeight; - + RowGroupSublevelIndents = []; + _rowGroupHeightsByLevel = []; UpdatePseudoClasses(); } + + /// + /// Enters editing mode for the current cell and current row (if they're not already in editing mode). + /// + /// Provides information about the user gesture that caused the call to BeginEdit. Can be null. + /// True if operation was successful. False otherwise. + 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); + } + + /// + /// Cancels editing mode for the specified DataGridEditingUnit and restores its original value. + /// + /// Specifies whether to cancel edit for a Cell or Row. + /// True if operation was successful. False otherwise. + public bool CancelEdit(DataGridEditingUnit editingUnit = DataGridEditingUnit.Row) + { + return CancelEdit(editingUnit, raiseEvents: true); + } + + /// + /// Commits editing mode and pushes changes to the backend. + /// + /// True if operation was successful. False otherwise. + public bool CommitEdit() + { + return CommitEdit(DataGridEditingUnit.Row, true); + } + + /// + /// Commits editing mode for the specified DataGridEditingUnit and pushes changes to the backend. + /// + /// Specifies whether to commit edit for a Cell or Row. + /// Editing mode is left if True. + /// True if operation was successful. False otherwise. + 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; + } + + /// + /// 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. + /// + /// an item from the DataGrid's items source or a CollectionViewGroup from the collection view + /// a column from the DataGrid's columns collection + 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 && column != 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); + 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); + } + } + + /// + /// Arranges the content of the . + /// + /// + /// The final area within the parent that this element should use to arrange itself and its children. + /// + /// + /// The actual size used by the . + /// + 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); + } + + /// + /// Measures the children of a to prepare for + /// arranging them during the + /// pass. + /// + /// + /// The size that the determines it needs during layout, based on its calculations of child object allocated sizes. + /// + /// + /// The available size that this element can give to child elements. Indicates an upper limit that + /// child elements should not exceed. + /// + 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; + } + + /// + protected override void OnDataContextBeginUpdate() + { + base.OnDataContextBeginUpdate(); + + NotifyDataContextPropertyForAllRowCells(GetAllRows(), true); + } + + /// + protected override void OnDataContextEndUpdate() + { + base.OnDataContextEndUpdate(); + + NotifyDataContextPropertyForAllRowCells(GetAllRows(), false); + } + + /// + /// Raises the BeginningEdit event. + /// + protected virtual void OnBeginningEdit(DataGridBeginningEditEventArgs e) + { + BeginningEdit?.Invoke(this, e); + } + + /// + /// Raises the CellEditEnded event. + /// + protected virtual void OnCellEditEnded(DataGridCellEditEndedEventArgs e) + { + CellEditEnded?.Invoke(this, e); + } + + /// + /// Raises the CellEditEnding event. + /// + protected virtual void OnCellEditEnding(DataGridCellEditEndingEventArgs e) + { + CellEditEnding?.Invoke(this, e); + } + + /// + /// Raises the CellPointerPressed event. + /// + internal virtual void OnCellPointerPressed(DataGridCellPointerPressedEventArgs e) + { + CellPointerPressed?.Invoke(this, e); + } + + /// + /// Raises the CurrentCellChanged event. + /// + protected virtual void OnCurrentCellChanged(EventArgs e) + { + CurrentCellChanged?.Invoke(this, e); + } + + /// + /// Raises the LoadingRow event for row preparation. + /// + protected virtual void OnLoadingRow(DataGridRowEventArgs e) + { + EventHandler? 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); + } + } + + /// + /// Scrolls the DataGrid according to the direction of the delta. + /// + /// PointerWheelEventArgs + 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 * 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; + } + + /// + /// Raises the PreparingCellForEdit event. + /// + protected virtual void OnPreparingCellForEdit(DataGridPreparingCellForEditEventArgs e) + { + PreparingCellForEdit?.Invoke(this, e); + } + + /// + /// Raises the RowEditEnded event. + /// + protected virtual void OnRowEditEnded(DataGridRowEditEndedEventArgs e) + { + RowEditEnded?.Invoke(this, e); + } + + /// + /// Raises the RowEditEnding event. + /// + protected virtual void OnRowEditEnding(DataGridRowEditEndingEventArgs e) + { + RowEditEnding?.Invoke(this, e); + } + + /// + /// Raises the SelectionChanged event and clears the _selectionChanged. + /// This event won't get raised again until after _selectionChanged is set back to true. + /// + protected virtual void OnSelectionChanged(SelectionChangedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Raises the UnloadingRow event for row recycling. + /// + protected virtual void OnUnloadingRow(DataGridRowEventArgs e) + { + EventHandler? handler = UnloadingRow; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + /// + /// Comparator class so we can sort list by the display index + /// + public class DisplayIndexComparer : IComparer + { + int IComparer.Compare(DataGridColumn? x, DataGridColumn? y) + { + if (x == null && y == null) + { + return 0; + } + if (x == null && y != null) + { + return -1; + } + + if (x != null && y == null) + { + return 1; + } + + Debug.Assert(x != null && y != null); + return (x.DisplayIndexWithFiller < y.DisplayIndexWithFiller) ? -1 : 1; + } + } + + /// + /// Builds the visual tree for the column header when a new template is applied. + /// + //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(DataGridTheme.ColumnHeadersPresenterPart); + + 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 sortedInternal = new List(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(DataGridTheme.RowsPresenterPart); + + if (_rowsPresenter != null) + { + _rowsPresenter.OwningGrid = this; + InvalidateRowHeightEstimate(); + UpdateRowDetailsHeightEstimate(); + } + + _frozenColumnScrollBarSpacer = e.NameScope.Find(DataGridTheme.FrozenColumnScrollBarSpacerPart); + + if (_hScrollBar != null) + { + _hScrollBar.Scroll -= HandleHorizontalScrollBarScroll; + } + + _hScrollBar = e.NameScope.Find(DataGridTheme.HorizontalScrollbarPart); + + if (_hScrollBar != null) + { + _hScrollBar.IsTabStop = false; + _hScrollBar.Maximum = 0.0; + _hScrollBar.Orientation = Orientation.Horizontal; + _hScrollBar.IsVisible = false; + _hScrollBar.Scroll += HandleHorizontalScrollBarScroll; + } + + if (_vScrollBar != null) + { + _vScrollBar.Scroll -= HandleVerticalScrollBarScroll; + } + + _vScrollBar = e.NameScope.Find(DataGridTheme.VerticalScrollbarPart); + + if (_vScrollBar != null) + { + _vScrollBar.IsTabStop = false; + _vScrollBar.Maximum = 0.0; + _vScrollBar.Orientation = Orientation.Vertical; + _vScrollBar.IsVisible = false; + _vScrollBar.Scroll += HandleVerticalScrollBarScroll; + } + + _topLeftCornerHeader = e.NameScope.Find(DataGridTheme.TopLeftCornerPart); + EnsureTopLeftCornerHeader(); // EnsureTopLeftCornerHeader checks for a null _topLeftCornerHeader; + _topRightCornerHeader = e.NameScope.Find(DataGridTheme.TopRightCornePart); + _bottomRightCorner = e.NameScope.Find(DataGridTheme.BottomRightCornerPart); + } + + /// + /// Cancels editing mode for the specified DataGridEditingUnit and restores its original value. + /// + /// Specifies whether to cancel edit for a Cell or Row. + /// Specifies whether or not to raise editing events + /// True if operation was successful. False otherwise. + 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; + } + + /// + /// call when: selection changes or SelectedItems object changes + /// + 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 IEnumerable 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 selectedItemsCache = new List(_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; + } + _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; + } + 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 + { + Debug.Assert(_hScrollBar != null); + 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); + } + + /// + /// Selects items and updates currency based on parameters + /// + /// column index to make current + /// data item or CollectionViewGroup to make current + /// slot to use in case the item is no longer valid + /// selection action to perform + /// whether or not the new current item should be scrolled into view + 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(_vScrollBar != null); + 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: !MathUtils.AreClose(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 (!MathUtils.AreClose(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; + } + // TODO 需要评估 + if (item == null) + { + 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); + } + } + + /// + /// 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. + /// + /// Action to perform after the editing element loses focus + /// True if the editing element had focus and the action was cached away; false otherwise + //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 += HandleEditingElementLostFocus; + //IsTabStop = true; + Focus(); + return true; + } + } + return false; + } + + /// + /// Raises the LoadingRowDetails for row details preparation + /// + protected virtual void NotifyLoadingRowDetails(DataGridRowDetailsEventArgs e) + { + EventHandler? handler = LoadingRowDetails; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + /// + /// Raises the UnloadingRowDetails event + /// + protected virtual void NotifyUnloadingRowDetails(DataGridRowDetailsEventArgs e) + { + EventHandler? 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 rowSource, bool arg2) + { + foreach (DataGridRow row in rowSource) + { + foreach (DataGridCell cell in row.Cells) + { + if (cell.Content is StyledElement cellContent) + { + // TODO Error 需要重构 + //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(CurrentColumn != null); + 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(); + } + + /// + /// Handles the current editing element's LostFocus event by performing any actions that + /// were cached by the WaitForLostFocus method. + /// + /// Editing element + /// RoutedEventArgs + private void HandleEditingElementLostFocus(object? sender, RoutedEventArgs e) + { + if (sender is Control editingElement) + { + editingElement.LostFocus -= HandleEditingElementLostFocus; + 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: 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 = FocusUtils.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 += HandleExternalEditingElementLostFocus; + } + } + } + } + + private void HandleEditingElementInitialized(object? sender, EventArgs e) + { + if (sender is Control element) + { + element.Initialized -= HandleEditingElementInitialized; + 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); + Debug.Assert(CurrentColumn != null); + + // 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) + { + Debug.Assert(CurrentColumn != null); + 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) + { + Debug.Assert(_uneditedValue != null); + 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(); + DataValidationErrors.ClearErrors(editingElement); + } + else + { + if (editingCell.IsValid) + { + editingCell.IsValid = false; + editingCell.UpdatePseudoClasses(); + } + + if (editingRow.IsValid) + { + editingRow.IsValid = false; + editingRow.ApplyState(); + } + + 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(); + Debug.Assert(CurrentColumn != null); + 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; + Debug.Assert(column != null); + 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; + } + } + + /// + /// Exits editing mode without trying to commit or revert the editing, and + /// without repopulating the edited row's cell. + /// + //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 HandleExternalEditingElementLostFocus(object? sender, RoutedEventArgs e) + { + if (sender is Control element) + { + element.LostFocus -= HandleExternalEditingElementLostFocus; + DataGrid_LostFocus(sender, e); + } + } + + 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; + } + // 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); + } + // 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 HandleHorizontalScrollBarScroll(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; + Debug.Assert(dataGridRow.DataContext != null); + 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 += HandleEditingElementInitialized; + } + } + } + else + { + // Generate Element and apply column style if available + element = dataGridColumn.GenerateElementInternal(dataGridCell, dataGridRow.DataContext); + dataGridCell.Content = element; + } + } + + private void PreparingCellForEditPrivate(Control editingElement) + { + Debug.Assert(EditingRow != null); + 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; + Debug.Assert(dataGridColumn != null); + Debug.Assert(_editingEventArgs != null); + _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 (FocusUtils.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; + } + 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(); + } + Debug.Assert(ColumnsInternal.FillerColumn != null); + 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 (!MathUtils.AreClose(_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 HandleVerticalScrollBarScroll(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; + } + + /// + /// Returns the Group at the indicated level or null if the item is not in the ItemsSource + /// + /// item + /// groupLevel + /// The group the given item falls under or null if the item is not in the ItemsSource + 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; + } + + /// + /// Raises the LoadingRowGroup event + /// + /// EventArgs + protected virtual void OnLoadingRowGroup(DataGridRowGroupHeaderEventArgs e) + { + EventHandler? handler = LoadingRowGroup; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + /// + /// Raises the UnLoadingRowGroup event + /// + /// EventArgs + protected virtual void OnUnloadingRowGroup(DataGridRowGroupHeaderEventArgs e) + { + EventHandler? handler = UnloadingRowGroup; + if (handler != null) + { + LoadingOrUnloadingRow = true; + handler(this, e); + LoadingOrUnloadingRow = false; + } + } + + // 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; + } + previousHeaderSlot = RowGroupHeadersTable.GetPreviousIndex(previousHeaderSlot); + } + } + + /// + /// This method raises the CopyingRowClipboardContent event. + /// + /// Contains the necessary information for generating the row clipboard content. + protected virtual void OnCopyingRowClipboardContent(DataGridRowClipboardEventArgs e) + { + CopyingRowClipboardContent?.Invoke(this, e); + } + + /// + /// 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. + /// + /// DataGridRowClipboardEventArgs + /// The formatted string. + 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); + } + + /// + /// 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. + /// + /// Whether or not the DataGrid handled the key press. + 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++) + { + var 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); + } + + /// + /// 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. + /// + 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; + } + + /// + /// Raises the AutoGeneratingColumn event. + /// + protected virtual void OnAutoGeneratingColumn(DataGridAutoGeneratingColumnEventArgs e) + { + AutoGeneratingColumn?.Invoke(this, e); + } } \ No newline at end of file