// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using Avalonia.Collections;
using Avalonia.Controls.Utils;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Controls
{
public partial class DataGrid
{
internal bool AreRowBottomGridLinesRequired
{
get
{
return (GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || GridLinesVisibility == DataGridGridLinesVisibility.All) && HorizontalGridLinesBrush != null;
}
}
internal int FirstVisibleSlot
{
get
{
return (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
{
get
{
return (SlotCount > 0) ? GetPreviousVisibleSlot(SlotCount) : -1;
}
}
// 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);
// 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 (DataConnection != null && 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 (DataGridRow row in DisplayData.GetScrollingRows())
{
row.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight);
}
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, CellsHeight);
}
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), CellsHeight))
{
// The entire row won't fit in the DataGrid so we start showing it from the top
NegVerticalOffset = 0;
UpdateDisplayedRows(slot, CellsHeight);
}
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);
OnAddedElement_Phase1(slot, element);
SlotCount++;
VisibleSlotCount++;
OnAddedElement_Phase2(slot, updateVerticalScrollBarOnly: false);
OnElementsChanged(grew: true);
}
private void AddSlots(int totalSlots)
{
SlotCount = 0;
VisibleSlotCount = 0;
IEnumerator groupSlots = null;
int nextGroupSlot = -1;
if (RowGroupHeadersTable.RangeCount > 0)
{
groupSlots = RowGroupHeadersTable.GetIndexes().GetEnumerator();
if (groupSlots != null && groupSlots.MoveNext())
{
nextGroupSlot = groupSlots.Current;
}
}
int slot = 0;
int addedRows = 0;
while (slot < totalSlots && AvailableSlotElementRoom > 0)
{
if (slot == nextGroupSlot)
{
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;
OnAddedElement_Phase2(0,
updateVerticalScrollBarOnly: _vScrollBar == null || _vScrollBar.IsVisible);
OnElementsChanged(grew: true);
}
}
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.UpdatePseudoClasses(); ;
}
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 (DataGridRow row in DisplayData.GetScrollingRows())
{
if (row.Slot > slotDeleted)
{
CorrectRowAfterDeletion(row, wasRow);
_rowsPresenter?.InvalidateChildIndex(row);
}
}
// Update the RowGroupHeaders
foreach (int slot in RowGroupHeadersTable.GetIndexes())
{
DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
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)
{
DataGrid.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)
{
DataGrid.CorrectRowAfterInsertion(_focusedRow, rowInserted);
}
// Take care of the visible rows
foreach (DataGridRow row in DisplayData.GetScrollingRows())
{
if (row.Slot >= slotInserted)
{
DataGrid.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);
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 = CellsHeight - 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 && EditingRow.Cells != null)
{
Debug.Assert(EditingRow.Cells.Count == ColumnsItemsInternal.Count);
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.TemplatedParent);
}
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
if (groupHeader != null)
{
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 && RowHeightEstimate == DataGrid.DATAGRID_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);
OnInsertingElement(slot, true /*firstInsertion*/, isCollapsed); // will throw an exception if the insertion is illegal
OnInsertedElement_Phase1(slot, element, isCollapsed, isRow);
SlotCount++;
if (!isCollapsed)
{
VisibleSlotCount++;
}
OnInsertedElement_Phase2(slot, updateVerticalScrollBarOnly, isCollapsed);
}
private void InvalidateRowHeightEstimate()
{
// Start from scratch and assume that we haven't estimated any rows
_lastEstimatedRow = -1;
}
private void OnAddedElement_Phase1(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 OnAddedElement_Phase2(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 OnElementsChanged(bool grew)
{
if (grew &&
ColumnsItemsInternal.Count > 0 &&
CurrentColumnIndex == -1)
{
MakeFirstDisplayedCellCurrentCell();
}
}
private void OnInsertedElement_Phase1(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);
OnAddedElement_Phase1(slot, element);
}
else if ((slot <= DisplayData.FirstScrollingSlot) || (isCollapsed && (slot <= DisplayData.LastScrollingSlot)))
{
DisplayData.CorrectSlotsAfterInsertion(slot, null /*row*/, isCollapsed);
}
}
private void OnInsertedElement_Phase2(int slot, bool updateVerticalScrollBarOnly, bool isCollapsed)
{
Debug.Assert(slot >= 0);
if (!isCollapsed)
{
// Same effect as adding a row
OnAddedElement_Phase2(slot, updateVerticalScrollBarOnly);
}
}
private void OnInsertingElement(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 OnRemovedElement(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.UpdatePseudoClasses();
}
// 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.AddRecylableRowGroupHeader(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 && EditingRow.Cells != 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);
OnRemovedElement(slot, item);
}
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 = CellsHeight;
}
///
/// 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;
}
else if (DisplayData.FirstScrollingSlot == -1 &&
CellsHeight > 0 &&
CellsWidth > 0)
{
return true;
}
else 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 * CellsHeight &&
(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 * CellsHeight &&
(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, CellsHeight);
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, CellsHeight);
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, CellsHeight);
}
}
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.UpdatePseudoClasses();
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 (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 = CellsHeight;
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 = CellsHeight;
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 CollectionViewGroup_CollectionChanged(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 (_rowGroupHeightsByLevel != null &&
DataConnection.CollectionView != null &&
DataConnection.CollectionView.IsGrouping &&
DataConnection.CollectionView.GroupingDepth == _rowGroupHeightsByLevel.Length)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
CollectionViewGroup_CollectionChanged_Add(sender, e);
break;
case NotifyCollectionChangedAction.Remove:
CollectionViewGroup_CollectionChanged_Remove(sender, e);
break;
}
}
}
private void CollectionViewGroup_CollectionChanged_Add(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)
{
if (group.Items != null)
{
group.Items.CollectionChanged += CollectionViewGroup_CollectionChanged;
}
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.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 CollectionViewGroup_CollectionChanged_Remove(object sender, NotifyCollectionChangedEventArgs e)
{
Debug.Assert(e.OldItems.Count == 1);
if (e.OldItems != null && e.OldItems.Count > 0)
{
if (e.OldItems[0] is DataGridCollectionViewGroup removedGroup)
{
if (removedGroup.Items != null)
{
removedGroup.Items.CollectionChanged -= CollectionViewGroup_CollectionChanged;
}
DataGridRowGroupInfo groupInfo = RowGroupInfoFromCollectionViewGroup(removedGroup);
Debug.Assert(groupInfo != null);
if ((groupInfo.Level == _rowGroupHeightsByLevel.Length - 1) && (removedGroup.Items != null) && (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 (parentGroupInfo.CollectionViewGroup == null && 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.Items != null)
{
groupInfo.CollectionViewGroup.Items.CollectionChanged -= CollectionViewGroup_CollectionChanged;
}
}
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 -= CollectionViewGroup_CollectionChanged;
_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 = null;
RowGroupSublevelIndents = null;
}
private void CollectionViewGroup_PropertyChanged(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 != null && collectionViewGroup.Items.Count > 0)
{
collectionViewGroup.Items.CollectionChanged += CollectionViewGroup_CollectionChanged;
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)
{
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 += CollectionViewGroup_CollectionChanged;
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 == null || _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] = DATAGRID_defaultRowHeight;
}
}
if (RowGroupSublevelIndents == null || RowGroupSublevelIndents.Length != groupLevelCount)
{
RowGroupSublevelIndents = new double[groupLevelCount];
double indent;
for (int i = 0; i < groupLevelCount; i++)
{
indent = DATAGRID_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(MathUtilities.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)))
{
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);
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, CellsHeight);
}
}
else
{
// Collapse
endSlot = SlotCount - 1;
foreach (int slot in RowGroupHeadersTable.GetIndexes(targetRowGroupInfo.Slot + 1))
{
DataGridRowGroupInfo rowGroupInfo = RowGroupHeadersTable.GetValueAt(slot);
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, CellsHeight);
}
}
}
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, CellsHeight);
}
}
}
}
// 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.TemplatedParent);
}
// 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(CollectionViewGroup_PropertyChanged);
inpc.PropertyChanged += new PropertyChangedEventHandler(CollectionViewGroup_PropertyChanged);
}
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);
}
else
{
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.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)
{
_rowsPresenter.Children.Add(detailsContent);
if (dataItem != null)
{
detailsContent.DataContext = dataItem;
}
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 OnUnloadingRowDetails(DataGridRow row, Control detailsElement)
{
OnUnloadingRowDetails(new DataGridRowDetailsEventArgs(row, detailsElement));
}
// detailsElement is the FrameworkElement created by the DetailsTemplate
internal void OnLoadingRowDetails(DataGridRow row, Control detailsElement)
{
OnLoadingRowDetails(new DataGridRowDetailsEventArgs(row, detailsElement));
}
internal void OnRowDetailsVisibilityPropertyChanged(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);
}
else
{
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
}
}