7493 lines
310 KiB
C#
7493 lines
310 KiB
C#
//---------------------------------------------------------------------------
|
|
//
|
|
// Copyright (C) Microsoft Corporation. All rights reserved.
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security;
|
|
using System.Text;
|
|
using System.Windows;
|
|
using System.Windows.Automation.Peers;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Data;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Threading;
|
|
using EGC = ExtendedGrid.Microsoft.Windows.Controls;
|
|
using MS.Internal;
|
|
|
|
namespace ExtendedGrid.Microsoft.Windows.Controls
|
|
{
|
|
/// <summary>
|
|
/// A DataGrid control that displays data in rows and columns and allows
|
|
/// for the entering and editing of data.
|
|
/// </summary>
|
|
public class DataGrid : MultiSelector
|
|
{
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Instantiates global information.
|
|
/// </summary>
|
|
static DataGrid()
|
|
{
|
|
Type ownerType = typeof(EGC.DataGrid);
|
|
|
|
DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(typeof(EGC.DataGrid)));
|
|
FrameworkElementFactory dataGridRowPresenterFactory = new FrameworkElementFactory(typeof(EGC.DataGridRowsPresenter));
|
|
dataGridRowPresenterFactory.SetValue(FrameworkElement.NameProperty, ItemsPanelPartName);
|
|
ItemsPanelProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(new ItemsPanelTemplate(dataGridRowPresenterFactory)));
|
|
VirtualizingStackPanel.IsVirtualizingProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(true, null, new CoerceValueCallback(OnCoerceIsVirtualizingProperty)));
|
|
VirtualizingStackPanel.VirtualizationModeProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(VirtualizationMode.Recycling));
|
|
ItemContainerStyleProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(null, new CoerceValueCallback(OnCoerceItemContainerStyle)));
|
|
ItemContainerStyleSelectorProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(null, new CoerceValueCallback(OnCoerceItemContainerStyleSelector)));
|
|
ItemsSourceProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata((PropertyChangedCallback)null, OnCoerceItemsSourceProperty));
|
|
AlternationCountProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(0, null, new CoerceValueCallback(OnCoerceAlternationCount)));
|
|
IsEnabledProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsEnabledChanged)));
|
|
IsSynchronizedWithCurrentItemProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(null, new CoerceValueCallback(OnCoerceIsSynchronizedWithCurrentItem)));
|
|
IsTabStopProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(false));
|
|
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(KeyboardNavigationMode.Contained));
|
|
KeyboardNavigation.ControlTabNavigationProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(KeyboardNavigationMode.Once));
|
|
|
|
CommandManager.RegisterClassInputBinding(ownerType, new InputBinding(BeginEditCommand, new KeyGesture(Key.F2)));
|
|
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(BeginEditCommand, new ExecutedRoutedEventHandler(OnExecutedBeginEdit), new CanExecuteRoutedEventHandler(OnCanExecuteBeginEdit)));
|
|
|
|
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(CommitEditCommand, new ExecutedRoutedEventHandler(OnExecutedCommitEdit), new CanExecuteRoutedEventHandler(OnCanExecuteCommitEdit)));
|
|
|
|
CommandManager.RegisterClassInputBinding(ownerType, new InputBinding(CancelEditCommand, new KeyGesture(Key.Escape)));
|
|
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(CancelEditCommand, new ExecutedRoutedEventHandler(OnExecutedCancelEdit), new CanExecuteRoutedEventHandler(OnCanExecuteCancelEdit)));
|
|
|
|
CommandManager.RegisterClassInputBinding(ownerType, new InputBinding(SelectAllCommand, EGC.DataGridHelper.CreateFromResourceStrings(SR.Get(SRID.DataGrid_SelectAllKey), SR.Get(SRID.DataGrid_SelectAllKeyDisplayString))));
|
|
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(SelectAllCommand, new ExecutedRoutedEventHandler(OnExecutedSelectAll), new CanExecuteRoutedEventHandler(OnCanExecuteSelectAll)));
|
|
|
|
CommandManager.RegisterClassInputBinding(ownerType, new InputBinding(DeleteCommand, new KeyGesture(Key.Delete)));
|
|
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(DeleteCommand, new ExecutedRoutedEventHandler(OnExecutedDelete), new CanExecuteRoutedEventHandler(OnCanExecuteDelete)));
|
|
|
|
// Default Clipboard handling
|
|
CommandManager.RegisterClassCommandBinding(typeof(EGC.DataGrid), new CommandBinding(ApplicationCommands.Copy, new ExecutedRoutedEventHandler(OnExecutedCopy), new CanExecuteRoutedEventHandler(OnCanExecuteCopy)));
|
|
|
|
EventManager.RegisterClassHandler(typeof(EGC.DataGrid), MouseUpEvent, new MouseButtonEventHandler(OnAnyMouseUpThunk), true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instantiates a new instance of this class.
|
|
/// </summary>
|
|
public DataGrid()
|
|
{
|
|
_columns = new EGC.DataGridColumnCollection(this);
|
|
_columns.CollectionChanged += new NotifyCollectionChangedEventHandler(OnColumnsChanged);
|
|
|
|
_rowValidationRules = new ObservableCollection<ValidationRule>();
|
|
_rowValidationRules.CollectionChanged += new NotifyCollectionChangedEventHandler(OnRowValidationRulesChanged);
|
|
|
|
_selectedCells = new EGC.SelectedCellsCollection(this);
|
|
|
|
((INotifyCollectionChanged)Items).CollectionChanged += new NotifyCollectionChangedEventHandler(OnItemsCollectionChanged);
|
|
|
|
((INotifyCollectionChanged)Items.SortDescriptions).CollectionChanged += new NotifyCollectionChangedEventHandler(OnItemsSortDescriptionsChanged);
|
|
Items.GroupDescriptions.CollectionChanged += new NotifyCollectionChangedEventHandler(OnItemsGroupDescriptionsChanged);
|
|
|
|
// Compute column widths but wait until first load
|
|
InternalColumns.InvalidateColumnWidthsComputation();
|
|
|
|
CellsPanelHorizontalOffsetComputationPending = false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Columns
|
|
|
|
/// <summary>
|
|
/// A collection of column definitions describing the individual
|
|
/// columns of each row.
|
|
/// </summary>
|
|
public ObservableCollection<EGC.DataGridColumn> Columns
|
|
{
|
|
get { return _columns; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the column collection without having to upcast from ObservableCollection
|
|
/// </summary>
|
|
internal EGC.DataGridColumnCollection InternalColumns
|
|
{
|
|
get { return _columns; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// A property that specifies whether the user can resize columns in the UI by dragging the column headers.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This does not affect whether column widths can be changed programmatically via a property such as Column.Width.
|
|
/// </remarks>
|
|
public bool CanUserResizeColumns
|
|
{
|
|
get { return (bool)GetValue(CanUserResizeColumnsProperty); }
|
|
set { SetValue(CanUserResizeColumnsProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the CanUserResizeColumns property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty CanUserResizeColumnsProperty =
|
|
DependencyProperty.Register("CanUserResizeColumns", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnNotifyColumnHeaderPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// Specifies the width of the header and cells within all the columns.
|
|
/// </summary>
|
|
public EGC.DataGridLength ColumnWidth
|
|
{
|
|
get { return (EGC.DataGridLength)GetValue(ColumnWidthProperty); }
|
|
set { SetValue(ColumnWidthProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the ColumnWidth property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty ColumnWidthProperty =
|
|
DependencyProperty.Register("ColumnWidth", typeof(EGC.DataGridLength), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridLength.SizeToHeader));
|
|
|
|
/// <summary>
|
|
/// Specifies the minimum width of the header and cells within all columns.
|
|
/// </summary>
|
|
public double MinColumnWidth
|
|
{
|
|
get { return (double)GetValue(MinColumnWidthProperty); }
|
|
set { SetValue(MinColumnWidthProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the MinColumnWidth property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty MinColumnWidthProperty =
|
|
DependencyProperty.Register(
|
|
"MinColumnWidth",
|
|
typeof(double),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(20d, new PropertyChangedCallback(OnColumnSizeConstraintChanged)),
|
|
new ValidateValueCallback(ValidateMinColumnWidth));
|
|
|
|
/// <summary>
|
|
/// Specifies the maximum width of the header and cells within all columns.
|
|
/// </summary>
|
|
public double MaxColumnWidth
|
|
{
|
|
get { return (double)GetValue(MaxColumnWidthProperty); }
|
|
set { SetValue(MaxColumnWidthProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the MaxColumnWidth property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty MaxColumnWidthProperty =
|
|
DependencyProperty.Register(
|
|
"MaxColumnWidth",
|
|
typeof(double),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(double.PositiveInfinity, new PropertyChangedCallback(OnColumnSizeConstraintChanged)),
|
|
new ValidateValueCallback(ValidateMaxColumnWidth));
|
|
|
|
private static void OnColumnSizeConstraintChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Columns);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that the minimum column width is an acceptable value
|
|
/// </summary>
|
|
private static bool ValidateMinColumnWidth(object v)
|
|
{
|
|
double value = (double)v;
|
|
return !(value < 0d || DoubleUtil.IsNaN(value) || Double.IsPositiveInfinity(value));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that the maximum column width is an acceptable value
|
|
/// </summary>
|
|
private static bool ValidateMaxColumnWidth(object v)
|
|
{
|
|
double value = (double)v;
|
|
return !(value < 0d || DoubleUtil.IsNaN(value));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the Columns collection changes.
|
|
/// </summary>
|
|
private void OnColumnsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
// Update the reference to this DataGrid on the affected column(s)
|
|
// and update the SelectedCells collection.
|
|
switch (e.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
UpdateDataGridReference(e.NewItems, /* clear = */ false);
|
|
UpdateColumnSizeConstraints(e.NewItems);
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Remove:
|
|
UpdateDataGridReference(e.OldItems, /* clear = */ true);
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Replace:
|
|
UpdateDataGridReference(e.OldItems, /* clear = */ true);
|
|
UpdateDataGridReference(e.NewItems, /* clear = */ false);
|
|
UpdateColumnSizeConstraints(e.NewItems);
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Reset:
|
|
// We can't clear column references on Reset: _columns has 0 items and e.OldItems is empty.
|
|
_selectedCells.Clear();
|
|
break;
|
|
}
|
|
|
|
// FrozenColumns rely on column DisplayIndex
|
|
// Delay the coercion if necessary
|
|
if (InternalColumns.DisplayIndexMapInitialized)
|
|
{
|
|
CoerceValue(FrozenColumnCountProperty);
|
|
}
|
|
|
|
bool visibleColumnsChanged = HasVisibleColumns(e.OldItems);
|
|
visibleColumnsChanged |= HasVisibleColumns(e.NewItems);
|
|
visibleColumnsChanged |= (e.Action == NotifyCollectionChangedAction.Reset);
|
|
|
|
if (visibleColumnsChanged)
|
|
{
|
|
InternalColumns.InvalidateColumnRealization(true);
|
|
}
|
|
|
|
UpdateColumnsOnRows(e);
|
|
|
|
// Recompute the column width if required, but wait until the first load
|
|
if (visibleColumnsChanged && e.Action != NotifyCollectionChangedAction.Move)
|
|
{
|
|
InternalColumns.InvalidateColumnWidthsComputation();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the reference to this DataGrid on the list of columns.
|
|
/// </summary>
|
|
/// <param name="list">The list of affected columns.</param>
|
|
/// <param name="clear">Whether to add or remove the reference to this grid.</param>
|
|
internal void UpdateDataGridReference(IList list, bool clear)
|
|
{
|
|
int numItems = list.Count;
|
|
for (int i = 0; i < numItems; i++)
|
|
{
|
|
EGC.DataGridColumn column = (EGC.DataGridColumn)list[i];
|
|
if (clear)
|
|
{
|
|
// Set the owner to null only if the current owner is this grid
|
|
if (column.DataGridOwner == this)
|
|
{
|
|
column.DataGridOwner = null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Remove the column from any old owner
|
|
if (column.DataGridOwner != null && column.DataGridOwner != this)
|
|
{
|
|
column.DataGridOwner.Columns.Remove(column);
|
|
}
|
|
|
|
column.DataGridOwner = this;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the transferred size constraints from DataGrid on the columns.
|
|
/// </summary>
|
|
/// <param name="list">The list of affected columns.</param>
|
|
private static void UpdateColumnSizeConstraints(IList list)
|
|
{
|
|
var count = list.Count;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var column = (EGC.DataGridColumn)list[i];
|
|
column.SyncProperties();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which determines if the
|
|
/// given list has visible columns
|
|
/// </summary>
|
|
private static bool HasVisibleColumns(IList columns)
|
|
{
|
|
if (columns != null && columns.Count > 0)
|
|
{
|
|
foreach (EGC.DataGridColumn column in columns)
|
|
{
|
|
if (column.IsVisible)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Display Index
|
|
|
|
/// <summary>
|
|
/// Returns the DataGridColumn with the given DisplayIndex
|
|
/// </summary>
|
|
public EGC.DataGridColumn ColumnFromDisplayIndex(int displayIndex)
|
|
{
|
|
if (displayIndex < 0 || displayIndex >= Columns.Count)
|
|
{
|
|
throw new ArgumentOutOfRangeException("displayIndex", displayIndex, SR.Get(SRID.DataGrid_DisplayIndexOutOfRange));
|
|
}
|
|
|
|
return InternalColumns.ColumnFromDisplayIndex(displayIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event that is fired when the DisplayIndex on one of the DataGrid's Columns changes.
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridColumnEventArgs> ColumnDisplayIndexChanged;
|
|
|
|
/// <summary>
|
|
/// Called when the DisplayIndex of a column is modified.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A column's DisplayIndex may be modified as the result of another column's DisplayIndex changing. This is because the
|
|
/// DataGrid enforces that the DisplayIndex of all Columns are unique integers from 0 to Columns.Count -1.
|
|
/// </remarks>
|
|
protected internal virtual void OnColumnDisplayIndexChanged(EGC.DataGridColumnEventArgs e)
|
|
{
|
|
if (ColumnDisplayIndexChanged != null)
|
|
{
|
|
ColumnDisplayIndexChanged(this, e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A map of display index (key) to index in the column collection (value).
|
|
/// Used by the CellsPanel to quickly find a child from a column display index.
|
|
/// </summary>
|
|
internal List<int> DisplayIndexMap
|
|
{
|
|
get { return InternalColumns.DisplayIndexMap; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Throws an ArgumentOutOfRangeException if the given displayIndex is invalid.
|
|
/// </summary>
|
|
internal void ValidateDisplayIndex(EGC.DataGridColumn column, int displayIndex)
|
|
{
|
|
InternalColumns.ValidateDisplayIndex(column, displayIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the index of a column from the given DisplayIndex
|
|
/// </summary>
|
|
internal int ColumnIndexFromDisplayIndex(int displayIndex)
|
|
{
|
|
if (displayIndex >= 0 && displayIndex < DisplayIndexMap.Count)
|
|
{
|
|
return DisplayIndexMap[displayIndex];
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given the DisplayIndex of a column returns the DataGridColumnHeader for that column.
|
|
/// Used by DataGridColumnHeader to find its previous sibling.
|
|
/// </summary>
|
|
/// <param name="displayIndex"></param>
|
|
/// <returns></returns>
|
|
internal EGC.DataGridColumnHeader ColumnHeaderFromDisplayIndex(int displayIndex)
|
|
{
|
|
int columnIndex = ColumnIndexFromDisplayIndex(displayIndex);
|
|
|
|
if (columnIndex != -1)
|
|
{
|
|
if (ColumnHeadersPresenter != null && ColumnHeadersPresenter.ItemContainerGenerator != null)
|
|
{
|
|
return (EGC.DataGridColumnHeader)ColumnHeadersPresenter.ItemContainerGenerator.ContainerFromIndex(columnIndex);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Notification Propagation
|
|
|
|
/// <summary>
|
|
/// Notifies each CellsPresenter about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyCellsPresenterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.CellsPresenter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies each Column and Cell about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyColumnAndCellPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Columns | NotificationTarget.Cells);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies each Column about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyColumnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Columns);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the Column & Column Headers about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyColumnAndColumnHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Columns | NotificationTarget.ColumnHeaders);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the Column Headers about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyColumnHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnHeaders);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the Row and Column Headers about property changes (used by the AlternationBackground property)
|
|
/// </summary>
|
|
private static void OnNotifyHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnHeaders | NotificationTarget.RowHeaders);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the DataGrid and each Row about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyDataGridAndRowPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Rows | NotificationTarget.DataGrid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies everyone who cares about GridLine property changes (Row, Cell, RowHeader, ColumnHeader)
|
|
/// </summary>
|
|
private static void OnNotifyGridLinePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
// Clear out and regenerate all containers. We do this so that we don't have to propagate this notification
|
|
// to containers that are currently on the recycle queue -- doing so costs us perf on every scroll. We don't
|
|
// care about the time spent on a GridLine change since it'll be a very rare occurance.
|
|
//
|
|
// ItemsControl.OnItemTemplateChanged calls the internal ItemContainerGenerator.Refresh() method, which
|
|
// clears out all containers and notifies the panel. The fact we're passing in two null templates is ignored.
|
|
if (e.OldValue != e.NewValue)
|
|
{
|
|
((EGC.DataGrid)d).OnItemTemplateChanged(null, null);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies each Row about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyRowPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Rows);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the Row Headers about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyRowHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.RowHeaders);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the Row & Row Headers about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyRowAndRowHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Rows | NotificationTarget.RowHeaders);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the Row & Details about property changes.
|
|
/// </summary>
|
|
private static void OnNotifyRowAndDetailsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Rows | NotificationTarget.DetailsPresenter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies HorizontalOffset change to columns collection, cellspresenter and column headers presenter
|
|
/// </summary>
|
|
private static void OnNotifyHorizontalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnCollection | NotificationTarget.CellsPresenter | NotificationTarget.ColumnHeadersPresenter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// General notification for DependencyProperty changes from the grid or from columns.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can be called from a variety of sources, such as from column objects
|
|
/// or from this DataGrid itself when there is a need to notify the rows and/or
|
|
/// the cells in the DataGrid about a property change. Down-stream handlers
|
|
/// can check the source of the change using the "d" parameter.
|
|
/// </remarks>
|
|
internal void NotifyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e, NotificationTarget target)
|
|
{
|
|
NotifyPropertyChanged(d, string.Empty, e, target);
|
|
}
|
|
|
|
/// <summary>
|
|
/// General notification for DependencyProperty changes from the grid or from columns.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can be called from a variety of sources, such as from column objects
|
|
/// or from this DataGrid itself when there is a need to notify the rows and/or
|
|
/// the cells in the DataGrid about a property change. Down-stream handlers
|
|
/// can check the source of the change using the "d" parameter.
|
|
/// </remarks>
|
|
internal void NotifyPropertyChanged(DependencyObject d, string propertyName, DependencyPropertyChangedEventArgs e, NotificationTarget target)
|
|
{
|
|
if (EGC.DataGridHelper.ShouldNotifyDataGrid(target))
|
|
{
|
|
if (e.Property == AlternatingRowBackgroundProperty)
|
|
{
|
|
// If the alternate row background is set, the count may be coerced to 2
|
|
CoerceValue(AlternationCountProperty);
|
|
}
|
|
}
|
|
|
|
// Rows, Cells, CellsPresenter, DetailsPresenter or RowHeaders
|
|
if (EGC.DataGridHelper.ShouldNotifyRowSubtree(target))
|
|
{
|
|
// Notify the Rows about the property change
|
|
EGC.ContainerTracking<EGC.DataGridRow> tracker = _rowTrackingRoot;
|
|
while (tracker != null)
|
|
{
|
|
tracker.Container.NotifyPropertyChanged(d, propertyName, e, target);
|
|
tracker = tracker.Next;
|
|
}
|
|
}
|
|
|
|
if (EGC.DataGridHelper.ShouldNotifyColumnCollection(target) || EGC.DataGridHelper.ShouldNotifyColumns(target))
|
|
{
|
|
InternalColumns.NotifyPropertyChanged(d, propertyName, e, target);
|
|
}
|
|
|
|
if ((EGC.DataGridHelper.ShouldNotifyColumnHeadersPresenter(target) || EGC.DataGridHelper.ShouldNotifyColumnHeaders(target)) && ColumnHeadersPresenter != null)
|
|
{
|
|
ColumnHeadersPresenter.NotifyPropertyChanged(d, propertyName, e, target);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by DataGridColumnCollection when columns' DisplayIndex changes
|
|
/// </summary>
|
|
/// <param name="e"></param>
|
|
internal void UpdateColumnsOnVirtualizedCellInfoCollections(NotifyCollectionChangedAction action, int oldDisplayIndex, EGC.DataGridColumn oldColumn, int newDisplayIndex)
|
|
{
|
|
using (UpdateSelectedCells())
|
|
{
|
|
_selectedCells.OnColumnsChanged(action, oldDisplayIndex, oldColumn, newDisplayIndex, SelectedItems);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reference to the ColumnHeadersPresenter. The presenter sets this when it is created.
|
|
/// </summary>
|
|
internal EGC.DataGridColumnHeadersPresenter ColumnHeadersPresenter
|
|
{
|
|
get { return _columnHeadersPresenter; }
|
|
set { _columnHeadersPresenter = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnTemplateChanged override
|
|
/// </summary>
|
|
protected override void OnTemplateChanged(ControlTemplate oldTemplate, ControlTemplate newTemplate)
|
|
{
|
|
base.OnTemplateChanged(oldTemplate, newTemplate);
|
|
|
|
// Our column headers presenter comes from the template. Clear out the reference to it if the template has changed
|
|
ColumnHeadersPresenter = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A cell is notifying the DataGrid that its IsKeyboardFocusWithin property changed.
|
|
/// </summary>
|
|
internal void CellIsKeyboardFocusWithinChanged(EGC.DataGridCell cell, bool isKeyboardFocusWithin)
|
|
{
|
|
UpdateCurrentCell(cell, isKeyboardFocusWithin);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GridLines
|
|
|
|
/// <summary>
|
|
/// GridLinesVisibility Dependency Property
|
|
/// </summary>
|
|
public static readonly DependencyProperty GridLinesVisibilityProperty =
|
|
DependencyProperty.Register(
|
|
"GridLinesVisibility",
|
|
typeof(EGC.DataGridGridLinesVisibility),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(EGC.DataGridGridLinesVisibility.All, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// Specifies the visibility of the DataGrid's grid lines
|
|
/// </summary>
|
|
public EGC.DataGridGridLinesVisibility GridLinesVisibility
|
|
{
|
|
get { return (EGC.DataGridGridLinesVisibility)GetValue(GridLinesVisibilityProperty); }
|
|
set { SetValue(GridLinesVisibilityProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// HorizontalGridLinesBrush Dependency Property
|
|
/// </summary>
|
|
public static readonly DependencyProperty HorizontalGridLinesBrushProperty =
|
|
DependencyProperty.Register(
|
|
"HorizontalGridLinesBrush",
|
|
typeof(Brush),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(Brushes.Black, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// Specifies the Brush used to draw the horizontal grid lines
|
|
/// </summary>
|
|
public Brush HorizontalGridLinesBrush
|
|
{
|
|
get { return (Brush)GetValue(HorizontalGridLinesBrushProperty); }
|
|
set { SetValue(HorizontalGridLinesBrushProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// VerticalGridLinesBrush Dependency Property
|
|
/// </summary>
|
|
public static readonly DependencyProperty VerticalGridLinesBrushProperty =
|
|
DependencyProperty.Register(
|
|
"VerticalGridLinesBrush",
|
|
typeof(Brush),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(Brushes.Black, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// Specifies the Brush used to draw the vertical grid lines
|
|
/// </summary>
|
|
public Brush VerticalGridLinesBrush
|
|
{
|
|
get { return (Brush)GetValue(VerticalGridLinesBrushProperty); }
|
|
set { SetValue(VerticalGridLinesBrushProperty, value); }
|
|
}
|
|
|
|
#if GridLineThickness
|
|
/// <summary>
|
|
/// HorizontalGridLineThickness DependencyProperty
|
|
/// </summary>
|
|
public static readonly DependencyProperty HorizontalGridLineThicknessProperty =
|
|
DependencyProperty.Register("HorizontalGridLineThickness", typeof(double), typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(1d, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// Specifies the thickness of the horizontal grid lines.
|
|
/// </summary>
|
|
public double HorizontalGridLineThickness
|
|
{
|
|
get { return (double)GetValue(HorizontalGridLineThicknessProperty); }
|
|
set { SetValue(HorizontalGridLineThicknessProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// VerticalGridLineThickness DependencyProperty
|
|
/// </summary>
|
|
public static readonly DependencyProperty VerticalGridLineThicknessProperty =
|
|
DependencyProperty.Register("VerticalGridLineThickness", typeof(double), typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(1d, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged)));
|
|
|
|
|
|
/// <summary>
|
|
/// Specifies the thickness of the vertical grid lines.
|
|
/// </summary>
|
|
public double VerticalGridLineThickness
|
|
{
|
|
get { return (double)GetValue(VerticalGridLineThicknessProperty); }
|
|
set { SetValue(VerticalGridLineThicknessProperty, value); }
|
|
}
|
|
|
|
#else
|
|
internal double HorizontalGridLineThickness
|
|
{
|
|
get { return 1.0; }
|
|
}
|
|
|
|
internal double VerticalGridLineThickness
|
|
{
|
|
get { return 1.0; }
|
|
}
|
|
#endif
|
|
|
|
#endregion
|
|
|
|
#region Row Generation
|
|
|
|
/// <summary>
|
|
/// Determines if an item is its own container.
|
|
/// </summary>
|
|
/// <param name="item">The item to test.</param>
|
|
/// <returns>true if the item is a DataGridRow, false otherwise.</returns>
|
|
protected override bool IsItemItsOwnContainerOverride(object item)
|
|
{
|
|
return item is EGC.DataGridRow;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instantiates an instance of a container.
|
|
/// </summary>
|
|
/// <returns>A new DataGridRow.</returns>
|
|
protected override DependencyObject GetContainerForItemOverride()
|
|
{
|
|
return new EGC.DataGridRow();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepares a new container for a given item.
|
|
/// </summary>
|
|
/// <param name="element">The new container.</param>
|
|
/// <param name="item">The item that the container represents.</param>
|
|
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
|
|
{
|
|
base.PrepareContainerForItemOverride(element, item);
|
|
|
|
EGC.DataGridRow row = (EGC.DataGridRow)element;
|
|
if (row.DataGridOwner != this)
|
|
{
|
|
row.Tracker.StartTracking(ref _rowTrackingRoot);
|
|
EnsureInternalScrollControls();
|
|
}
|
|
|
|
// We dont want changes to DetailsVisibilty to fire the LoadingRowDetails event during prepare, so we disable it.
|
|
row.DetailsEventStatus = EGC.DataGridRow.RowDetailsEventStatus.Disabled;
|
|
row.PrepareRow(item, this);
|
|
row.DetailsEventStatus = EGC.DataGridRow.RowDetailsEventStatus.Pending;
|
|
OnLoadingRow(new EGC.DataGridRowEventArgs(row));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears a container of references.
|
|
/// </summary>
|
|
/// <param name="element">The container being cleared.</param>
|
|
/// <param name="item">The data item that the container represented.</param>
|
|
protected override void ClearContainerForItemOverride(DependencyObject element, object item)
|
|
{
|
|
base.ClearContainerForItemOverride(element, item);
|
|
|
|
EGC.DataGridRow row = (EGC.DataGridRow)element;
|
|
if (row.DataGridOwner == this)
|
|
{
|
|
row.Tracker.StopTracking(ref _rowTrackingRoot);
|
|
}
|
|
|
|
OnUnloadingRow(new EGC.DataGridRowEventArgs(row));
|
|
row.ClearRow(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Propagates the collection changed notification on Columns down to
|
|
/// each active DataGridRow.
|
|
/// </summary>
|
|
/// <param name="e">The event arguments from the original collection changed event.</param>
|
|
private void UpdateColumnsOnRows(NotifyCollectionChangedEventArgs e)
|
|
{
|
|
EGC.ContainerTracking<EGC.DataGridRow> tracker = _rowTrackingRoot;
|
|
while (tracker != null)
|
|
{
|
|
tracker.Container.OnColumnsChanged(_columns, e);
|
|
tracker = tracker.Next;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Equivalent of ItemContainerStyle.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If this property has a non-null value, it will override the value
|
|
/// of ItemContainerStyle.
|
|
/// </remarks>
|
|
public Style RowStyle
|
|
{
|
|
get { return (Style)GetValue(RowStyleProperty); }
|
|
set { SetValue(RowStyleProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for the RowStyle property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowStyleProperty =
|
|
DependencyProperty.Register("RowStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnRowStyleChanged)));
|
|
|
|
private static void OnRowStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
d.CoerceValue(ItemContainerStyleProperty);
|
|
}
|
|
|
|
private static object OnCoerceItemContainerStyle(DependencyObject d, object baseValue)
|
|
{
|
|
if (!EGC.DataGridHelper.IsDefaultValue(d, EGC.DataGrid.RowStyleProperty))
|
|
{
|
|
return d.GetValue(EGC.DataGrid.RowStyleProperty);
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Template used to visually indicate an error in row Validation.
|
|
/// </summary>
|
|
public ControlTemplate RowValidationErrorTemplate
|
|
{
|
|
get { return (ControlTemplate)GetValue(RowValidationErrorTemplateProperty); }
|
|
set { SetValue(RowValidationErrorTemplateProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for the RowValidationErrorTemplate property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowValidationErrorTemplateProperty =
|
|
DependencyProperty.Register("RowValidationErrorTemplate", typeof(ControlTemplate), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// Validation rules that are run on each DataGridRow. If DataGrid.ItemBindingGroup is used, RowValidationRules is ignored.
|
|
/// </summary>
|
|
public ObservableCollection<ValidationRule> RowValidationRules
|
|
{
|
|
get { return _rowValidationRules; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the Columns collection changes.
|
|
/// </summary>
|
|
private void OnRowValidationRulesChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
BindingGroup itemBindingGroup = ItemBindingGroup;
|
|
|
|
if (itemBindingGroup == null)
|
|
{
|
|
ItemBindingGroup = itemBindingGroup = new BindingGroup();
|
|
_rowValidationBindingGroup = itemBindingGroup;
|
|
}
|
|
|
|
// only update the ItemBindingGroup if it's not user created.
|
|
if (_rowValidationBindingGroup != null)
|
|
{
|
|
if (object.ReferenceEquals(itemBindingGroup, _rowValidationBindingGroup))
|
|
{
|
|
switch (e.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
foreach (ValidationRule rule in e.NewItems)
|
|
{
|
|
_rowValidationBindingGroup.ValidationRules.Add(rule);
|
|
}
|
|
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Remove:
|
|
foreach (ValidationRule rule in e.OldItems)
|
|
{
|
|
_rowValidationBindingGroup.ValidationRules.Remove(rule);
|
|
}
|
|
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Replace:
|
|
foreach (ValidationRule rule in e.OldItems)
|
|
{
|
|
_rowValidationBindingGroup.ValidationRules.Remove(rule);
|
|
}
|
|
|
|
foreach (ValidationRule rule in e.NewItems)
|
|
{
|
|
_rowValidationBindingGroup.ValidationRules.Add(rule);
|
|
}
|
|
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Reset:
|
|
_rowValidationBindingGroup.ValidationRules.Clear();
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_rowValidationBindingGroup = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Equivalent of ItemContainerStyleSelector.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If this property has a non-null value, it will override the value
|
|
/// of ItemContainerStyleSelector.
|
|
/// </remarks>
|
|
public StyleSelector RowStyleSelector
|
|
{
|
|
get { return (StyleSelector)GetValue(RowStyleSelectorProperty); }
|
|
set { SetValue(RowStyleSelectorProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for the RowStyleSelector property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowStyleSelectorProperty =
|
|
DependencyProperty.Register("RowStyleSelector", typeof(StyleSelector), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnRowStyleSelectorChanged)));
|
|
|
|
private static void OnRowStyleSelectorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
d.CoerceValue(ItemContainerStyleSelectorProperty);
|
|
}
|
|
|
|
private static object OnCoerceItemContainerStyleSelector(DependencyObject d, object baseValue)
|
|
{
|
|
if (!EGC.DataGridHelper.IsDefaultValue(d, EGC.DataGrid.RowStyleSelectorProperty))
|
|
{
|
|
return d.GetValue(EGC.DataGrid.RowStyleSelectorProperty);
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
private static object OnCoerceIsSynchronizedWithCurrentItem(DependencyObject d, object baseValue)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
if (dataGrid.SelectionUnit == EGC.DataGridSelectionUnit.Cell)
|
|
{
|
|
// IsSynchronizedWithCurrentItem makes IsSelected=true on the current row.
|
|
// When SelectionUnit is Cell, we should not allow row selection.
|
|
return false;
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The default row background brush.
|
|
/// </summary>
|
|
public Brush RowBackground
|
|
{
|
|
get { return (Brush)GetValue(RowBackgroundProperty); }
|
|
set { SetValue(RowBackgroundProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for RowBackground.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowBackgroundProperty =
|
|
DependencyProperty.Register("RowBackground", typeof(Brush), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The default row background brush for use on every other row.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Setting this property to a non-null value will coerce AlternationCount to 2.
|
|
/// </remarks>
|
|
public Brush AlternatingRowBackground
|
|
{
|
|
get { return (Brush)GetValue(AlternatingRowBackgroundProperty); }
|
|
set { SetValue(AlternatingRowBackgroundProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for AlternatingRowBackground.
|
|
/// </summary>
|
|
public static readonly DependencyProperty AlternatingRowBackgroundProperty =
|
|
DependencyProperty.Register("AlternatingRowBackground", typeof(Brush), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyDataGridAndRowPropertyChanged)));
|
|
|
|
private static object OnCoerceAlternationCount(DependencyObject d, object baseValue)
|
|
{
|
|
// Only check AlternatingRowBackground if the value isn't already set
|
|
// to something that can use it.
|
|
if (((int)baseValue) < 2)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
if (dataGrid.AlternatingRowBackground != null)
|
|
{
|
|
// There is an alternate background, coerce to 2.
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The default height of a row.
|
|
/// </summary>
|
|
public double RowHeight
|
|
{
|
|
get { return (double)GetValue(RowHeightProperty); }
|
|
set { SetValue(RowHeightProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for RowHeight.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowHeightProperty =
|
|
DependencyProperty.Register("RowHeight", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(double.NaN, new PropertyChangedCallback(OnNotifyCellsPresenterPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The default minimum height of a row.
|
|
/// </summary>
|
|
public double MinRowHeight
|
|
{
|
|
get { return (double)GetValue(MinRowHeightProperty); }
|
|
set { SetValue(MinRowHeightProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for MinRowHeight.
|
|
/// </summary>
|
|
public static readonly DependencyProperty MinRowHeightProperty =
|
|
DependencyProperty.Register("MinRowHeight", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnNotifyCellsPresenterPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The NewItemPlaceholder row uses this to set its visibility while it's preparing.
|
|
/// </summary>
|
|
internal Visibility PlaceholderVisibility
|
|
{
|
|
get
|
|
{
|
|
return _placeholderVisibility;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event that is fired just before a row is prepared.
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridRowEventArgs> LoadingRow;
|
|
|
|
/// <summary>
|
|
/// Event that is fired just before a row is cleared.
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridRowEventArgs> UnloadingRow;
|
|
|
|
/// <summary>
|
|
/// Invokes the LoadingRow event
|
|
/// </summary>
|
|
protected virtual void OnLoadingRow(EGC.DataGridRowEventArgs e)
|
|
{
|
|
if (LoadingRow != null)
|
|
{
|
|
LoadingRow(this, e);
|
|
}
|
|
|
|
var row = e.Row;
|
|
if (row.DetailsVisibility == Visibility.Visible && row.DetailsPresenter != null)
|
|
{
|
|
// Invoke LoadingRowDetails, but only after the details template is expanded (so DetailsElement will be available).
|
|
// We dont want changes to DetailsVisibilty to fire the event before this dispatcher operation fires, so we disable it.
|
|
row.DetailsEventStatus = EGC.DataGridRow.RowDetailsEventStatus.Disabled;
|
|
Dispatcher.CurrentDispatcher.BeginInvoke(new DispatcherOperationCallback(DelayedOnLoadingRowDetails), DispatcherPriority.Loaded, row);
|
|
}
|
|
}
|
|
|
|
private static object DelayedOnLoadingRowDetails(object arg)
|
|
{
|
|
var row = (EGC.DataGridRow)arg;
|
|
var dataGrid = row.DataGridOwner;
|
|
|
|
if (dataGrid != null && row.DetailsPresenter != null)
|
|
{
|
|
dataGrid.OnLoadingRowDetails(new EGC.DataGridRowDetailsEventArgs(row, row.DetailsPresenter.DetailsElement));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the UnloadingRow event
|
|
/// </summary>
|
|
protected virtual void OnUnloadingRow(EGC.DataGridRowEventArgs e)
|
|
{
|
|
if (UnloadingRow != null)
|
|
{
|
|
UnloadingRow(this, e);
|
|
}
|
|
|
|
var row = e.Row;
|
|
if (row.DetailsEventStatus == EGC.DataGridRow.RowDetailsEventStatus.Loaded && row.DetailsPresenter != null)
|
|
{
|
|
OnUnloadingRowDetails(new EGC.DataGridRowDetailsEventArgs(row, row.DetailsPresenter.DetailsElement));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Row/Column Headers
|
|
|
|
/// <summary>
|
|
/// The default width of a row header.
|
|
/// </summary>
|
|
public double RowHeaderWidth
|
|
{
|
|
get { return (double)GetValue(RowHeaderWidthProperty); }
|
|
set { SetValue(RowHeaderWidthProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for RowHeaderWidth.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowHeaderWidthProperty =
|
|
DependencyProperty.Register("RowHeaderWidth", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(double.NaN, new PropertyChangedCallback(OnNotifyRowHeaderWidthPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The actual width of row headers used for binding. This is computed from the measure of all the visible row headers.
|
|
/// </summary>
|
|
public double RowHeaderActualWidth
|
|
{
|
|
get { return (double)GetValue(RowHeaderActualWidthProperty); }
|
|
internal set { SetValue(RowHeaderActualWidthPropertyKey, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyPropertyKey for RowHeaderActualWidth.
|
|
/// </summary>
|
|
private static readonly DependencyPropertyKey RowHeaderActualWidthPropertyKey =
|
|
DependencyProperty.RegisterReadOnly("RowHeaderActualWidth", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnNotifyRowHeaderPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for RowHeaderActualWidth.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowHeaderActualWidthProperty = RowHeaderActualWidthPropertyKey.DependencyProperty;
|
|
|
|
/// <summary>
|
|
/// The default height of a column header.
|
|
/// </summary>
|
|
public double ColumnHeaderHeight
|
|
{
|
|
get { return (double)GetValue(ColumnHeaderHeightProperty); }
|
|
set { SetValue(ColumnHeaderHeightProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for ColumnHeaderHeight.
|
|
/// </summary>
|
|
public static readonly DependencyProperty ColumnHeaderHeightProperty =
|
|
DependencyProperty.Register("ColumnHeaderHeight", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(double.NaN, OnNotifyColumnHeaderPropertyChanged));
|
|
|
|
/// <summary>
|
|
/// A property that specifies the visibility of the column & row headers.
|
|
/// </summary>
|
|
public EGC.DataGridHeadersVisibility HeadersVisibility
|
|
{
|
|
get { return (EGC.DataGridHeadersVisibility)GetValue(HeadersVisibilityProperty); }
|
|
set { SetValue(HeadersVisibilityProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the HeadersVisibility property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty HeadersVisibilityProperty =
|
|
DependencyProperty.Register("HeadersVisibility", typeof(EGC.DataGridHeadersVisibility), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(DataGridHeadersVisibility.All));
|
|
|
|
/// <summary>
|
|
/// Updates RowHeaderActualWidth to reflect changes to RowHeaderWidth
|
|
/// </summary>
|
|
private static void OnNotifyRowHeaderWidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
var dataGrid = ((EGC.DataGrid)d);
|
|
var newValue = (double)e.NewValue;
|
|
|
|
if (!DoubleUtil.IsNaN(newValue))
|
|
{
|
|
dataGrid.RowHeaderActualWidth = newValue;
|
|
}
|
|
else
|
|
{
|
|
// If we're entering Auto mode we need to reset the RowHeaderActualWidth
|
|
// because the previous explicit value may have been bigger than the Auto width.
|
|
dataGrid.RowHeaderActualWidth = 0.0;
|
|
}
|
|
|
|
OnNotifyRowHeaderPropertyChanged(d, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the RowHeaderActualWidth to 0.0 if in Auto mode
|
|
/// </summary>
|
|
private void ResetRowHeaderActualWidth()
|
|
{
|
|
if (DoubleUtil.IsNaN(RowHeaderWidth))
|
|
{
|
|
RowHeaderActualWidth = 0.0;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Item Associated Properties
|
|
|
|
/// <summary>
|
|
/// Sets the specified item's DetailsVisibility.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is useful when a DataGridRow may not currently exists to set DetailsVisibility on.
|
|
/// </remarks>
|
|
/// <param name="item">The item that will have its DetailsVisibility set.</param>
|
|
/// <param name="detailsVisibility">The Visibility that the item's details should get.</param>
|
|
public void SetDetailsVisibilityForItem(object item, Visibility detailsVisibility)
|
|
{
|
|
_itemAttachedStorage.SetValue(item, EGC.DataGridRow.DetailsVisibilityProperty, detailsVisibility);
|
|
|
|
var row = (EGC.DataGridRow)ItemContainerGenerator.ContainerFromItem(item);
|
|
if (row != null)
|
|
{
|
|
row.DetailsVisibility = detailsVisibility;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the current DetailsVisibility for an item that's in the DataGrid.
|
|
/// </summary>
|
|
/// <param name="item">The item who's DetailsVisibility you would like to get</param>
|
|
/// <returns>The DetailsVisibility associated with the specified item.</returns>
|
|
public Visibility GetDetailsVisibilityForItem(object item)
|
|
{
|
|
object detailsVisibility;
|
|
if (_itemAttachedStorage.TryGetValue(item, EGC.DataGridRow.DetailsVisibilityProperty, out detailsVisibility))
|
|
{
|
|
return (Visibility)detailsVisibility;
|
|
}
|
|
|
|
var row = (EGC.DataGridRow)ItemContainerGenerator.ContainerFromItem(item);
|
|
if (row != null)
|
|
{
|
|
return row.DetailsVisibility;
|
|
}
|
|
|
|
// If we dont have a row, we can infer it's Visibility from the current RowDetailsVisibilityMode
|
|
switch (RowDetailsVisibilityMode)
|
|
{
|
|
case EGC.DataGridRowDetailsVisibilityMode.VisibleWhenSelected:
|
|
return SelectedItems.Contains(item) ? Visibility.Visible : Visibility.Collapsed;
|
|
case EGC.DataGridRowDetailsVisibilityMode.Visible:
|
|
return Visibility.Visible;
|
|
default:
|
|
return Visibility.Collapsed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the DetailsVisibility for the specified item
|
|
/// </summary>
|
|
/// <param name="item">The item to clear the DetailsVisibility on.</param>
|
|
public void ClearDetailsVisibilityForItem(object item)
|
|
{
|
|
_itemAttachedStorage.ClearValue(item, EGC.DataGridRow.DetailsVisibilityProperty);
|
|
|
|
var row = (EGC.DataGridRow)ItemContainerGenerator.ContainerFromItem(item);
|
|
if (row != null)
|
|
{
|
|
row.ClearValue(EGC.DataGridRow.DetailsVisibilityProperty);
|
|
}
|
|
}
|
|
|
|
internal EGC.DataGridItemAttachedStorage ItemAttachedStorage
|
|
{
|
|
get { return _itemAttachedStorage; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Style Properties
|
|
|
|
/// <summary>
|
|
/// A style to apply to all cells in the DataGrid.
|
|
/// </summary>
|
|
public Style CellStyle
|
|
{
|
|
get { return (Style)GetValue(CellStyleProperty); }
|
|
set { SetValue(CellStyleProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the CellStyle property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty CellStyleProperty =
|
|
DependencyProperty.Register("CellStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyColumnAndCellPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// A style to apply to all column headers in the DataGrid
|
|
/// </summary>
|
|
public Style ColumnHeaderStyle
|
|
{
|
|
get { return (Style)GetValue(ColumnHeaderStyleProperty); }
|
|
set { SetValue(ColumnHeaderStyleProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the ColumnHeaderStyle property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty ColumnHeaderStyleProperty =
|
|
DependencyProperty.Register("ColumnHeaderStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyColumnAndColumnHeaderPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// A style to apply to all row headers in the DataGrid
|
|
/// </summary>
|
|
public Style RowHeaderStyle
|
|
{
|
|
get { return (Style)GetValue(RowHeaderStyleProperty); }
|
|
set { SetValue(RowHeaderStyleProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the RowHeaderStyle property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowHeaderStyleProperty =
|
|
DependencyProperty.Register("RowHeaderStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowAndRowHeaderPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The object representing the Row Header template.
|
|
/// </summary>
|
|
public DataTemplate RowHeaderTemplate
|
|
{
|
|
get { return (DataTemplate)GetValue(RowHeaderTemplateProperty); }
|
|
set { SetValue(RowHeaderTemplateProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for the RowHeaderTemplate property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowHeaderTemplateProperty =
|
|
DependencyProperty.Register("RowHeaderTemplate", typeof(DataTemplate), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowAndRowHeaderPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The object representing the Row Header template selector.
|
|
/// </summary>
|
|
public DataTemplateSelector RowHeaderTemplateSelector
|
|
{
|
|
get { return (DataTemplateSelector)GetValue(RowHeaderTemplateSelectorProperty); }
|
|
set { SetValue(RowHeaderTemplateSelectorProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for the RowHeaderTemplateSelector property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowHeaderTemplateSelectorProperty =
|
|
DependencyProperty.Register("RowHeaderTemplateSelector", typeof(DataTemplateSelector), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowAndRowHeaderPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The default style references this brush to create a thicker border
|
|
/// around the focused cell.
|
|
/// </summary>
|
|
public static ComponentResourceKey FocusBorderBrushKey
|
|
{
|
|
get
|
|
{
|
|
if (_focusBorderBrushKey == null)
|
|
{
|
|
_focusBorderBrushKey = new ComponentResourceKey(typeof(EGC.DataGrid), "FocusBorderBrushKey");
|
|
}
|
|
|
|
return _focusBorderBrushKey;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A converter which converts DataGridHeadersVisibility to VisibilityConverter based on a ConverterParameter.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can be used in the DataGrid's template to control which parts of the DataGrid are visible for a given DataGridHeadersVisibility.
|
|
/// </remarks>
|
|
public static IValueConverter HeadersVisibilityConverter
|
|
{
|
|
get
|
|
{
|
|
// This is delay created in case the template doesn't use it.
|
|
if (_headersVisibilityConverter == null)
|
|
{
|
|
_headersVisibilityConverter = new EGC.DataGridHeadersVisibilityToVisibilityConverter();
|
|
}
|
|
|
|
return _headersVisibilityConverter;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A converter which converts bool to SelectiveScrollingOrientation based on a ConverterParameter.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can be used in the DataGrid's template to control how the RowDetails selectively scroll based on a bool.
|
|
/// </remarks>
|
|
public static IValueConverter RowDetailsScrollingConverter
|
|
{
|
|
get
|
|
{
|
|
// This is delay created in case the template doesn't use it.
|
|
if (_rowDetailsScrollingConverter == null)
|
|
{
|
|
_rowDetailsScrollingConverter = new EGC.BooleanToSelectiveScrollingOrientationConverter();
|
|
}
|
|
|
|
return _rowDetailsScrollingConverter;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Scrolling
|
|
|
|
/// <summary>
|
|
/// Defines the behavior that determines the visibility of horizontal ScrollBars.
|
|
/// </summary>
|
|
public ScrollBarVisibility HorizontalScrollBarVisibility
|
|
{
|
|
get { return (ScrollBarVisibility)GetValue(HorizontalScrollBarVisibilityProperty); }
|
|
set { SetValue(HorizontalScrollBarVisibilityProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for the HorizontalScrollBarVisibility property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty HorizontalScrollBarVisibilityProperty = ScrollViewer.HorizontalScrollBarVisibilityProperty.AddOwner(typeof(EGC.DataGrid), new FrameworkPropertyMetadata(ScrollBarVisibility.Auto));
|
|
|
|
/// <summary>
|
|
/// Defines the behavior that determines the visibility of vertical ScrollBars.
|
|
/// </summary>
|
|
public ScrollBarVisibility VerticalScrollBarVisibility
|
|
{
|
|
get { return (ScrollBarVisibility)GetValue(VerticalScrollBarVisibilityProperty); }
|
|
set { SetValue(VerticalScrollBarVisibilityProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for the HorizontalScrollBarVisibility property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty VerticalScrollBarVisibilityProperty = ScrollViewer.VerticalScrollBarVisibilityProperty.AddOwner(typeof(EGC.DataGrid), new FrameworkPropertyMetadata(ScrollBarVisibility.Auto));
|
|
|
|
/// <summary>
|
|
/// Scrolls a row into view.
|
|
/// </summary>
|
|
/// <param name="item">The data item of the row to bring into view.</param>
|
|
public void ScrollIntoView(object item)
|
|
{
|
|
if (item == null)
|
|
{
|
|
throw new ArgumentNullException("item");
|
|
}
|
|
|
|
if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
|
|
{
|
|
ScrollRowIntoView(item);
|
|
}
|
|
else
|
|
{
|
|
// The items aren't generated, try at a later time
|
|
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(OnScrollIntoView), item);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scrolls a cell into view.
|
|
/// If column is null then only vertical scroll is performed.
|
|
/// If row is null then only horizontal scroll is performed.
|
|
/// </summary>
|
|
/// <param name="item">The data item row that contains the cell.</param>
|
|
/// <param name="column">The cell's column.</param>
|
|
public void ScrollIntoView(object item, EGC.DataGridColumn column)
|
|
{
|
|
if (column == null)
|
|
{
|
|
ScrollIntoView(item);
|
|
return;
|
|
}
|
|
|
|
if (!column.IsVisible)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
|
|
{
|
|
// Scroll by column only
|
|
if (item == null)
|
|
{
|
|
ScrollColumnIntoView(column);
|
|
}
|
|
else
|
|
{
|
|
ScrollCellIntoView(item, column);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The items aren't generated, try at a later time
|
|
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(OnScrollIntoView), new object[] { item, column });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Previous call to ScrollIntoView found that the generator had not finished
|
|
/// generating cells. This is the callback at Loaded priority when hopefully
|
|
/// that has occured.
|
|
/// </summary>
|
|
private object OnScrollIntoView(object arg)
|
|
{
|
|
object[] arguments = arg as object[];
|
|
if (arguments != null)
|
|
{
|
|
if (arguments[0] != null)
|
|
{
|
|
ScrollCellIntoView(arguments[0], (EGC.DataGridColumn)arguments[1]);
|
|
}
|
|
else
|
|
{
|
|
ScrollColumnIntoView((EGC.DataGridColumn)arguments[1]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ScrollRowIntoView(arg);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void ScrollColumnIntoView(EGC.DataGridColumn column)
|
|
{
|
|
if (_rowTrackingRoot != null)
|
|
{
|
|
EGC.DataGridRow row = _rowTrackingRoot.Container;
|
|
if (row != null)
|
|
{
|
|
int columnIndex = _columns.IndexOf(column);
|
|
row.ScrollCellIntoView(columnIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Consider making a protected virtual so that sub-classes can customize the behavior
|
|
private void ScrollRowIntoView(object item)
|
|
{
|
|
FrameworkElement element = ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
|
|
if (element != null)
|
|
{
|
|
element.BringIntoView();
|
|
}
|
|
else if (!IsGrouping)
|
|
{
|
|
// We might be virtualized, try scrolling by index.
|
|
int index = Items.IndexOf(item);
|
|
if (index >= 0)
|
|
{
|
|
// It would be convenient for ItemsHost to be public, but since it is not,
|
|
// we are relying on some internal communication between DataGridRowsPresenter
|
|
// and the DataGrid.
|
|
EGC.DataGridRowsPresenter itemsHost = InternalItemsHost as EGC.DataGridRowsPresenter;
|
|
if (itemsHost != null)
|
|
{
|
|
// It would have been better to be able to directly call BringIndexIntoView,
|
|
// but that method is protected, so we are relying on an internal
|
|
// method on DataGridRowsPresenter to make the call.
|
|
itemsHost.InternalBringIndexIntoView(index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Consider making a protected virtual so that sub-classes can customize the behavior
|
|
private void ScrollCellIntoView(object item, EGC.DataGridColumn column)
|
|
{
|
|
Debug.Assert(item != null, "item is null.");
|
|
Debug.Assert(column != null, "column is null.");
|
|
|
|
if (!column.IsVisible)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Devirtualize the concerned row if it is not already
|
|
EGC.DataGridRow row = ItemContainerGenerator.ContainerFromItem(item) as EGC.DataGridRow;
|
|
if (row == null)
|
|
{
|
|
ScrollRowIntoView(item);
|
|
UpdateLayout();
|
|
row = ItemContainerGenerator.ContainerFromItem(item) as EGC.DataGridRow;
|
|
}
|
|
|
|
// Use the row to scroll cell into view.
|
|
if (row != null)
|
|
{
|
|
int columnIndex = _columns.IndexOf(column);
|
|
row.ScrollCellIntoView(columnIndex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when IsMouseCaptured changes on this element.
|
|
/// </summary>
|
|
protected override void OnIsMouseCapturedChanged(DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (!IsMouseCaptured)
|
|
{
|
|
// When capture is lost, stop auto-scrolling
|
|
StopAutoScroll();
|
|
}
|
|
|
|
base.OnIsMouseCapturedChanged(e);
|
|
}
|
|
|
|
private static TimeSpan AutoScrollTimeout
|
|
{
|
|
get
|
|
{
|
|
// NOTE: NtUser does the following (file: windows/ntuser/kernel/sysmet.c)
|
|
// gpsi->dtLBSearch = dtTime * 4; // dtLBSearch = 4 * gdtDblClk
|
|
// gpsi->dtScroll = gpsi->dtLBSearch / 5; // dtScroll = 4/5 * gdtDblClk
|
|
return TimeSpan.FromMilliseconds(NativeMethods.GetDoubleClickTime() * 0.8);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begins a timer that will periodically scroll and select.
|
|
/// </summary>
|
|
private void StartAutoScroll()
|
|
{
|
|
if (_autoScrollTimer == null)
|
|
{
|
|
_hasAutoScrolled = false;
|
|
|
|
// Same priority as ListBox. Currently choosing SystemIdle over ApplicationIdle since the layout
|
|
// manger will do some work (sometimes) at ApplicationIdle.
|
|
_autoScrollTimer = new DispatcherTimer(DispatcherPriority.SystemIdle);
|
|
_autoScrollTimer.Interval = AutoScrollTimeout;
|
|
_autoScrollTimer.Tick += new EventHandler(OnAutoScrollTimeout);
|
|
_autoScrollTimer.Start();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the timer that controls auto-scrolling.
|
|
/// </summary>
|
|
private void StopAutoScroll()
|
|
{
|
|
if (_autoScrollTimer != null)
|
|
{
|
|
_autoScrollTimer.Stop();
|
|
_autoScrollTimer = null;
|
|
_hasAutoScrolled = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The callback when the auto-scroll timer ticks.
|
|
/// </summary>
|
|
private void OnAutoScrollTimeout(object sender, EventArgs e)
|
|
{
|
|
if (Mouse.LeftButton == MouseButtonState.Pressed)
|
|
{
|
|
DoAutoScroll();
|
|
}
|
|
else
|
|
{
|
|
StopAutoScroll();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Based on the mouse position relative to the rows and cells,
|
|
/// scrolls and selects rows and/or cells.
|
|
/// </summary>
|
|
/// <returns>true if a scroll and select was attempted. false otherwise.</returns>
|
|
private bool DoAutoScroll()
|
|
{
|
|
Debug.Assert(_isDraggingSelection, "DoAutoScroll should only be called when dragging selection.");
|
|
|
|
RelativeMousePositions position = RelativeMousePosition;
|
|
if (position != RelativeMousePositions.Over)
|
|
{
|
|
// Get the cell that is nearest the mouse position and is
|
|
// not being clipped by the ScrollViewer.
|
|
EGC.DataGridCell cell = GetCellNearMouse();
|
|
if (cell != null)
|
|
{
|
|
EGC.DataGridColumn column = cell.Column;
|
|
object dataItem = cell.RowDataItem;
|
|
|
|
// Based on the position of the mouse relative to the field
|
|
// of cells, choose the cell that is torwards the mouse.
|
|
// Note: This assumes a grid layout.
|
|
if (IsMouseToLeft(position))
|
|
{
|
|
int columnIndex = column.DisplayIndex;
|
|
if (columnIndex > 0)
|
|
{
|
|
column = ColumnFromDisplayIndex(columnIndex - 1);
|
|
}
|
|
}
|
|
else if (IsMouseToRight(position))
|
|
{
|
|
int columnIndex = column.DisplayIndex;
|
|
if (columnIndex < (_columns.Count - 1))
|
|
{
|
|
column = ColumnFromDisplayIndex(columnIndex + 1);
|
|
}
|
|
}
|
|
|
|
if (IsMouseAbove(position))
|
|
{
|
|
int rowIndex = Items.IndexOf(dataItem);
|
|
if (rowIndex > 0)
|
|
{
|
|
dataItem = Items[rowIndex - 1];
|
|
}
|
|
}
|
|
else if (IsMouseBelow(position))
|
|
{
|
|
int rowIndex = Items.IndexOf(dataItem);
|
|
if (rowIndex < (Items.Count - 1))
|
|
{
|
|
dataItem = Items[rowIndex + 1];
|
|
}
|
|
}
|
|
|
|
if (_isRowDragging)
|
|
{
|
|
// Perform a row header drag-select
|
|
ScrollRowIntoView(dataItem);
|
|
EGC.DataGridRow row = (EGC.DataGridRow)ItemContainerGenerator.ContainerFromItem(dataItem);
|
|
if (row != null)
|
|
{
|
|
_hasAutoScrolled = true;
|
|
HandleSelectionForRowHeaderAndDetailsInput(row, /* startDragging = */ false);
|
|
CurrentItem = dataItem;
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Perform a cell drag-select
|
|
ScrollCellIntoView(dataItem, column);
|
|
cell = TryFindCell(dataItem, column);
|
|
if (cell != null)
|
|
{
|
|
_hasAutoScrolled = true;
|
|
HandleSelectionForCellInput(cell, /* startDragging = */ false, /* allowsExtendSelect = */ true, /* allowsMinimalSelect = */ true);
|
|
cell.Focus();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prevents the ScrollViewer from handling keyboard input.
|
|
/// </summary>
|
|
protected override bool HandlesScrolling
|
|
{
|
|
get { return true; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Workaround for not having access to ItemsControl.ItemsHost.
|
|
/// </summary>
|
|
internal Panel InternalItemsHost
|
|
{
|
|
get { return _internalItemsHost; }
|
|
set
|
|
{
|
|
if (_internalItemsHost != value)
|
|
{
|
|
_internalItemsHost = value;
|
|
if (_internalItemsHost != null)
|
|
{
|
|
EnsureInternalScrollControls();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Workaround for not having access to ItemsControl.ScrollHost.
|
|
/// </summary>
|
|
internal ScrollViewer InternalScrollHost
|
|
{
|
|
get
|
|
{
|
|
EnsureInternalScrollControls();
|
|
return _internalScrollHost;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Workaround for not having access to ScrollContentPresenter
|
|
/// </summary>
|
|
internal ScrollContentPresenter InternalScrollContentPresenter
|
|
{
|
|
get
|
|
{
|
|
EnsureInternalScrollControls();
|
|
return _internalScrollContentPresenter;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which ensures the initialization of scroll controls.
|
|
/// </summary>
|
|
private void EnsureInternalScrollControls()
|
|
{
|
|
if (_internalScrollContentPresenter == null)
|
|
{
|
|
if (_internalItemsHost != null)
|
|
{
|
|
_internalScrollContentPresenter = EGC.DataGridHelper.FindVisualParent<ScrollContentPresenter>(_internalItemsHost);
|
|
}
|
|
else if (_rowTrackingRoot != null)
|
|
{
|
|
EGC.DataGridRow row = _rowTrackingRoot.Container;
|
|
_internalScrollContentPresenter = EGC.DataGridHelper.FindVisualParent<ScrollContentPresenter>(row);
|
|
}
|
|
if (_internalScrollContentPresenter != null)
|
|
{
|
|
_internalScrollContentPresenter.SizeChanged += new SizeChangedEventHandler(OnInternalScrollContentPresenterSizeChanged);
|
|
}
|
|
}
|
|
|
|
if (_internalScrollHost == null)
|
|
{
|
|
if (_internalItemsHost != null)
|
|
{
|
|
_internalScrollHost = EGC.DataGridHelper.FindVisualParent<ScrollViewer>(_internalItemsHost);
|
|
}
|
|
else if (_rowTrackingRoot != null)
|
|
{
|
|
EGC.DataGridRow row = _rowTrackingRoot.Container;
|
|
_internalScrollHost = EGC.DataGridHelper.FindVisualParent<ScrollViewer>(row);
|
|
}
|
|
if (_internalScrollHost != null)
|
|
{
|
|
Binding horizontalOffsetBinding = new Binding("ContentHorizontalOffset");
|
|
horizontalOffsetBinding.Source = _internalScrollHost;
|
|
SetBinding(HorizontalScrollOffsetProperty, horizontalOffsetBinding);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which cleans up the internal scroll controls.
|
|
/// </summary>
|
|
private void CleanUpInternalScrollControls()
|
|
{
|
|
BindingOperations.ClearBinding(this, HorizontalScrollOffsetProperty);
|
|
_internalScrollHost = null;
|
|
if (_internalScrollContentPresenter != null)
|
|
{
|
|
_internalScrollContentPresenter.SizeChanged -= new SizeChangedEventHandler(OnInternalScrollContentPresenterSizeChanged);
|
|
_internalScrollContentPresenter = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Size changed handler for InteralScrollContentPresenter.
|
|
/// </summary>
|
|
private void OnInternalScrollContentPresenterSizeChanged(object sender, SizeChangedEventArgs e)
|
|
{
|
|
if (_internalScrollContentPresenter != null &&
|
|
!_internalScrollContentPresenter.CanContentScroll)
|
|
{
|
|
OnViewportSizeChanged(e.PreviousSize, e.NewSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which enqueues a viewport width change
|
|
/// request to Dispatcher if needed.
|
|
/// </summary>
|
|
internal void OnViewportSizeChanged(Size oldSize, Size newSize)
|
|
{
|
|
if (!InternalColumns.ColumnWidthsComputationPending)
|
|
{
|
|
double widthChange = newSize.Width - oldSize.Width;
|
|
if (!DoubleUtil.AreClose(widthChange, 0.0))
|
|
{
|
|
_finalViewportWidth = newSize.Width;
|
|
if (!_viewportWidthChangeNotificationPending)
|
|
{
|
|
_originalViewportWidth = oldSize.Width;
|
|
Dispatcher.BeginInvoke(new DispatcherOperationCallback(OnDelayedViewportWidthChanged), DispatcherPriority.Loaded, this);
|
|
_viewportWidthChangeNotificationPending = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispatcher callback method for Viewport width change
|
|
/// which propagates the notification if needed.
|
|
/// </summary>
|
|
private object OnDelayedViewportWidthChanged(object args)
|
|
{
|
|
if (!_viewportWidthChangeNotificationPending)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
double widthChange = _finalViewportWidth - _originalViewportWidth;
|
|
if (!DoubleUtil.AreClose(widthChange, 0.0))
|
|
{
|
|
NotifyPropertyChanged(this,
|
|
"ViewportWidth",
|
|
new DependencyPropertyChangedEventArgs(),
|
|
NotificationTarget.CellsPresenter | NotificationTarget.ColumnHeadersPresenter | NotificationTarget.ColumnCollection);
|
|
|
|
double totalAvailableWidth = _finalViewportWidth;
|
|
totalAvailableWidth -= CellsPanelHorizontalOffset;
|
|
InternalColumns.RedistributeColumnWidthsOnAvailableSpaceChange(widthChange, totalAvailableWidth);
|
|
}
|
|
_viewportWidthChangeNotificationPending = false;
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dependency property which would be bound to ContentHorizontalOffset
|
|
/// property of the ScrollViewer.
|
|
/// </summary>
|
|
internal static readonly DependencyProperty HorizontalScrollOffsetProperty =
|
|
DependencyProperty.Register(
|
|
"HorizontalScrollOffset",
|
|
typeof(double),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(0d, OnNotifyHorizontalOffsetPropertyChanged));
|
|
|
|
/// <summary>
|
|
/// The HorizontalOffset of the scroll viewer
|
|
/// </summary>
|
|
internal double HorizontalScrollOffset
|
|
{
|
|
get
|
|
{
|
|
return (double)GetValue(HorizontalScrollOffsetProperty);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Editing Commands
|
|
|
|
/// <summary>
|
|
/// The command to fire and allow to route to the DataGrid in order to indicate that the
|
|
/// current cell or row should begin editing.
|
|
/// </summary>
|
|
public static readonly RoutedCommand BeginEditCommand = new RoutedCommand(SR.Get(SRID.DataGrid_BeginEditCommandText), typeof(EGC.DataGrid));
|
|
|
|
/// <summary>
|
|
/// The command to fire and allow to route to the DataGrid in order to indicate that the
|
|
/// current cell or row should commit any pending changes and exit edit mode.
|
|
/// </summary>
|
|
public static readonly RoutedCommand CommitEditCommand = new RoutedCommand(SR.Get(SRID.DataGrid_CommitEditCommandText), typeof(EGC.DataGrid));
|
|
|
|
/// <summary>
|
|
/// The command to fire and allow to route to the DataGrid in order to indicate that the
|
|
/// current cell or row should purge any pending changes and revert to the state it was
|
|
/// in before BeginEdit.
|
|
/// </summary>
|
|
public static readonly RoutedCommand CancelEditCommand = new RoutedCommand(SR.Get(SRID.DataGrid_CancelEditCommandText), typeof(EGC.DataGrid));
|
|
|
|
/// <summary>
|
|
/// A command that, when invoked, will delete the current row.
|
|
/// </summary>
|
|
public static readonly RoutedCommand DeleteCommand = new RoutedCommand(SR.Get(SRID.DataGrid_DeleteCommandText), typeof(EGC.DataGrid));
|
|
|
|
private static void OnCanExecuteBeginEdit(object sender, CanExecuteRoutedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnCanExecuteBeginEdit(e);
|
|
}
|
|
|
|
private static void OnExecutedBeginEdit(object sender, ExecutedRoutedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnExecutedBeginEdit(e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked to determine if the BeginEdit command can be executed.
|
|
/// </summary>
|
|
protected virtual void OnCanExecuteBeginEdit(CanExecuteRoutedEventArgs e)
|
|
{
|
|
bool canExecute = !IsReadOnly && (CurrentCellContainer != null) && !IsEditingCurrentCell && !IsCurrentCellReadOnly && !HasCellValidationError;
|
|
|
|
if (canExecute && HasRowValidationError)
|
|
{
|
|
EGC.DataGridCell cellContainer = GetEventCellOrCurrentCell(e);
|
|
if (cellContainer != null)
|
|
{
|
|
object rowItem = cellContainer.RowDataItem;
|
|
|
|
// When there is a validation error, only allow editing on that row
|
|
canExecute = IsAddingOrEditingRowItem(rowItem);
|
|
}
|
|
else
|
|
{
|
|
// Don't allow entering edit mode when there is a pending validation error
|
|
canExecute = false;
|
|
}
|
|
}
|
|
|
|
e.CanExecute = canExecute;
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when the BeginEdit command is executed.
|
|
/// </summary>
|
|
protected virtual void OnExecutedBeginEdit(ExecutedRoutedEventArgs e)
|
|
{
|
|
EGC.DataGridCell cell = CurrentCellContainer;
|
|
if ((cell != null) && !cell.IsReadOnly && !cell.IsEditing)
|
|
{
|
|
bool addedPlaceholder = false;
|
|
bool deselectedPlaceholder = false;
|
|
bool reselectPlaceholderCells = false;
|
|
List<int> columnIndexRanges = null;
|
|
int newItemIndex = -1;
|
|
object newItem = null;
|
|
bool placeholderAtBeginning = (EditableItems.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning);
|
|
|
|
if (IsNewItemPlaceholder(cell.RowDataItem))
|
|
{
|
|
// If editing the new item placeholder, then create a new item and edit that instead.
|
|
if (SelectedItems.Contains(CollectionView.NewItemPlaceholder))
|
|
{
|
|
// Unselect the NewItemPlaceholder and select the new row
|
|
UnselectItem(CollectionView.NewItemPlaceholder);
|
|
deselectedPlaceholder = true;
|
|
}
|
|
else
|
|
{
|
|
// Cells will automatically unselect when the new item placeholder is removed, but we
|
|
// should reselect them on the new item.
|
|
newItemIndex = Items.IndexOf(cell.RowDataItem);
|
|
reselectPlaceholderCells = ((newItemIndex >= 0) && _selectedCells.Intersects(newItemIndex, out columnIndexRanges));
|
|
}
|
|
|
|
newItem = AddNewItem();
|
|
CurrentItem = newItem; // Puts focus on the added row
|
|
cell = CurrentCellContainer;
|
|
if (CurrentCellContainer == null)
|
|
{
|
|
// CurrentCellContainer becomes null if focus moves out of the datagrid
|
|
// Calling UpdateLayout instantiates the CurrentCellContainer
|
|
UpdateLayout();
|
|
cell = CurrentCellContainer;
|
|
if ((cell != null) && !cell.IsKeyboardFocusWithin)
|
|
{
|
|
cell.Focus();
|
|
}
|
|
}
|
|
|
|
if (deselectedPlaceholder)
|
|
{
|
|
// Re-select the new item if the placeholder was selected before
|
|
SelectItem(newItem);
|
|
}
|
|
else if (reselectPlaceholderCells)
|
|
{
|
|
// Re-select placeholder cells if they were selected before
|
|
using (UpdateSelectedCells())
|
|
{
|
|
int rowIndex = newItemIndex;
|
|
|
|
// When the placeholder is at the beginning, we don't hide it, so those cells need to be unselected.
|
|
// The cells to select are also now one row below.
|
|
if (placeholderAtBeginning)
|
|
{
|
|
_selectedCells.RemoveRegion(newItemIndex, 0, 1, Columns.Count);
|
|
rowIndex++;
|
|
}
|
|
|
|
for (int i = 0, count = columnIndexRanges.Count; i < count; i += 2)
|
|
{
|
|
_selectedCells.AddRegion(rowIndex, columnIndexRanges[i], 1, columnIndexRanges[i + 1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
addedPlaceholder = true;
|
|
}
|
|
|
|
RoutedEventArgs editingEventArgs = e.Parameter as RoutedEventArgs;
|
|
EGC.DataGridBeginningEditEventArgs beginningEditEventArgs = null;
|
|
|
|
if (cell != null)
|
|
{
|
|
// Give the callback an opportunity to cancel edit mode
|
|
beginningEditEventArgs = new EGC.DataGridBeginningEditEventArgs(cell.Column, cell.RowOwner, editingEventArgs);
|
|
OnBeginningEdit(beginningEditEventArgs);
|
|
}
|
|
|
|
if ((cell == null) || beginningEditEventArgs.Cancel)
|
|
{
|
|
// If CurrentCellContainer is null then cancel editing
|
|
if (deselectedPlaceholder)
|
|
{
|
|
// If the new item placeholder was deselected and the new item was selected,
|
|
// de-select the new item. Selecting the new item placeholder comes at the end.
|
|
// This is to accomodate the scenario where the new item placeholder only appears
|
|
// when not editing a new item.
|
|
UnselectItem(newItem);
|
|
}
|
|
else if (reselectPlaceholderCells && placeholderAtBeginning)
|
|
{
|
|
// When the placeholder is at the beginning, we need to unselect the added item cells.
|
|
_selectedCells.RemoveRegion(newItemIndex + 1, 0, 1, Columns.Count);
|
|
}
|
|
|
|
if (addedPlaceholder)
|
|
{
|
|
// The edit was canceled, cancel the new item
|
|
CancelRowItem();
|
|
|
|
// Display the new item placeholder again
|
|
UpdateNewItemPlaceholder(/* isAddingNewItem = */ false);
|
|
|
|
// Put focus back on the placeholder
|
|
SetCurrentItemToPlaceholder();
|
|
}
|
|
|
|
if (deselectedPlaceholder)
|
|
{
|
|
// If the new item placeholder was deselected, then select it again.
|
|
SelectItem(CollectionView.NewItemPlaceholder);
|
|
}
|
|
else if (reselectPlaceholderCells)
|
|
{
|
|
for (int i = 0, count = columnIndexRanges.Count; i < count; i += 2)
|
|
{
|
|
_selectedCells.AddRegion(newItemIndex, columnIndexRanges[i], 1, columnIndexRanges[i + 1]);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!addedPlaceholder && !IsEditingRowItem)
|
|
{
|
|
EditRowItem(cell.RowDataItem);
|
|
|
|
var bindingGroup = cell.RowOwner.BindingGroup;
|
|
if (bindingGroup != null)
|
|
{
|
|
bindingGroup.BeginEdit();
|
|
}
|
|
|
|
_editingRowItem = cell.RowDataItem;
|
|
_editingRowIndex = Items.IndexOf(_editingRowItem);
|
|
}
|
|
|
|
cell.BeginEdit(editingEventArgs);
|
|
cell.RowOwner.IsEditing = true;
|
|
}
|
|
}
|
|
|
|
// CancelEdit and CommitEdit rely on IsAddingNewItem and IsEditingRowItem
|
|
CommandManager.InvalidateRequerySuggested();
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private static void OnCanExecuteCommitEdit(object sender, CanExecuteRoutedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnCanExecuteCommitEdit(e);
|
|
}
|
|
|
|
private static void OnExecutedCommitEdit(object sender, ExecutedRoutedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnExecutedCommitEdit(e);
|
|
}
|
|
|
|
private EGC.DataGridCell GetEventCellOrCurrentCell(RoutedEventArgs e)
|
|
{
|
|
// If the command routed through a cell, then use that cell. Otherwise, use the current cell.
|
|
UIElement source = e.OriginalSource as UIElement;
|
|
return ((source == this) || (source == null)) ? CurrentCellContainer : EGC.DataGridHelper.FindVisualParent<EGC.DataGridCell>(source);
|
|
}
|
|
|
|
private bool CanEndEdit(CanExecuteRoutedEventArgs e, bool commit)
|
|
{
|
|
EGC.DataGridCell cellContainer = GetEventCellOrCurrentCell(e);
|
|
if (cellContainer == null)
|
|
{
|
|
// If there is no cell, then nothing can be determined. So, no edit could end.
|
|
return false;
|
|
}
|
|
|
|
EGC.DataGridEditingUnit editingUnit = GetEditingUnit(e.Parameter);
|
|
IEditableCollectionView editableItems = EditableItems;
|
|
object rowItem = cellContainer.RowDataItem;
|
|
|
|
// Check that there is an appropriate pending add or edit.
|
|
// - If any cell is in edit mode
|
|
// - OR If the editing unit is row AND one of:
|
|
// - There is a pending add OR
|
|
// - There is a pending edit
|
|
return cellContainer.IsEditing ||
|
|
((editingUnit == EGC.DataGridEditingUnit.Row) &&
|
|
!HasCellValidationError &&
|
|
((editableItems.IsAddingNew && (editableItems.CurrentAddItem == rowItem)) ||
|
|
(editableItems.IsEditingItem && (commit || editableItems.CanCancelEdit || HasRowValidationError) && (editableItems.CurrentEditItem == rowItem))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked to determine if the CommitEdit command can be executed.
|
|
/// </summary>
|
|
protected virtual void OnCanExecuteCommitEdit(CanExecuteRoutedEventArgs e)
|
|
{
|
|
e.CanExecute = CanEndEdit(e, /* commit = */ true);
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when the CommitEdit command is executed.
|
|
/// </summary>
|
|
protected virtual void OnExecutedCommitEdit(ExecutedRoutedEventArgs e)
|
|
{
|
|
EGC.DataGridCell cell = CurrentCellContainer;
|
|
bool validationPassed = true;
|
|
if (cell != null)
|
|
{
|
|
EGC.DataGridEditingUnit editingUnit = GetEditingUnit(e.Parameter);
|
|
|
|
bool eventCanceled = false;
|
|
if (cell.IsEditing)
|
|
{
|
|
EGC.DataGridCellEditEndingEventArgs cellEditEndingEventArgs = new EGC.DataGridCellEditEndingEventArgs(cell.Column, cell.RowOwner, cell.EditingElement, EGC.DataGridEditAction.Commit);
|
|
OnCellEditEnding(cellEditEndingEventArgs);
|
|
|
|
eventCanceled = cellEditEndingEventArgs.Cancel;
|
|
if (!eventCanceled)
|
|
{
|
|
validationPassed = cell.CommitEdit();
|
|
HasCellValidationError = !validationPassed;
|
|
}
|
|
}
|
|
|
|
// Consider commiting the row if:
|
|
// 1. Validation passed on the cell or no cell was in edit mode.
|
|
// 2. A cell in edit mode didn't have it's ending edit event canceled.
|
|
// 3. The row can be commited:
|
|
// a. The row is being edited or added and being targeted directly.
|
|
// b. If the row is being edited (not added), but it doesn't support
|
|
// pending changes, then tell the row to commit (this doesn't really
|
|
// do anything except update the IEditableCollectionView state)
|
|
if (validationPassed &&
|
|
!eventCanceled &&
|
|
(((editingUnit == EGC.DataGridEditingUnit.Row) && IsAddingOrEditingRowItem(cell.RowDataItem)) ||
|
|
(!EditableItems.CanCancelEdit && IsEditingItem(cell.RowDataItem))))
|
|
{
|
|
EGC.DataGridRowEditEndingEventArgs rowEditEndingEventArgs = new EGC.DataGridRowEditEndingEventArgs(cell.RowOwner, EGC.DataGridEditAction.Commit);
|
|
OnRowEditEnding(rowEditEndingEventArgs);
|
|
|
|
if (!rowEditEndingEventArgs.Cancel)
|
|
{
|
|
var bindingGroup = cell.RowOwner.BindingGroup;
|
|
if (bindingGroup != null)
|
|
{
|
|
// CommitEdit will invoke the bindingGroup's ValidationRule's, so we need to make sure that all of the BindingExpressions
|
|
// have already registered with the BindingGroup. Synchronously flushing the Dispatcher to DataBind priority lets us ensure this.
|
|
// Had we used BeginInvoke instead, IsEditing would not reflect the correct value.
|
|
Dispatcher.Invoke(new DispatcherOperationCallback(DoNothing), DispatcherPriority.DataBind, bindingGroup);
|
|
validationPassed = bindingGroup.CommitEdit();
|
|
}
|
|
|
|
HasRowValidationError = !validationPassed;
|
|
if (validationPassed)
|
|
{
|
|
CommitRowItem();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (validationPassed)
|
|
{
|
|
// Update the state of row editing
|
|
UpdateRowEditing(cell);
|
|
}
|
|
|
|
// CancelEdit and CommitEdit rely on IsAddingNewItem and IsEditingRowItem
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is a helper method used to flush the dispatcher down to DataBind priority so that the bindingGroup will be ready for CommitEdit.
|
|
/// </summary>
|
|
/// <param name="arg"></param>
|
|
/// <returns></returns>
|
|
private static object DoNothing(object arg)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
private EGC.DataGridEditingUnit GetEditingUnit(object parameter)
|
|
{
|
|
// If the parameter contains a DataGridEditingUnit, then use it.
|
|
// Otherwise, choose Cell if a cell is currently being edited, or Row if not.
|
|
return ((parameter != null) && (parameter is EGC.DataGridEditingUnit)) ?
|
|
(EGC.DataGridEditingUnit)parameter :
|
|
IsEditingCurrentCell ? EGC.DataGridEditingUnit.Cell : EGC.DataGridEditingUnit.Row;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised just before row editing is ended.
|
|
/// Gives handlers the opportunity to cancel the operation.
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridRowEditEndingEventArgs> RowEditEnding;
|
|
|
|
/// <summary>
|
|
/// Called just before row editing is ended.
|
|
/// Gives subclasses the opportunity to cancel the operation.
|
|
/// </summary>
|
|
protected virtual void OnRowEditEnding(EGC.DataGridRowEditEndingEventArgs e)
|
|
{
|
|
if (RowEditEnding != null)
|
|
{
|
|
RowEditEnding(this, e);
|
|
}
|
|
|
|
if (AutomationPeer.ListenerExists(AutomationEvents.InvokePatternOnInvoked))
|
|
{
|
|
EGC.DataGridAutomationPeer peer = EGC.DataGridAutomationPeer.FromElement(this) as EGC.DataGridAutomationPeer;
|
|
if (peer != null)
|
|
{
|
|
peer.RaiseAutomationRowInvokeEvents(e.Row);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised just before cell editing is ended.
|
|
/// Gives handlers the opportunity to cancel the operation.
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridCellEditEndingEventArgs> CellEditEnding;
|
|
|
|
/// <summary>
|
|
/// Called just before cell editing is ended.
|
|
/// Gives subclasses the opportunity to cancel the operation.
|
|
/// </summary>
|
|
protected virtual void OnCellEditEnding(EGC.DataGridCellEditEndingEventArgs e)
|
|
{
|
|
if (CellEditEnding != null)
|
|
{
|
|
CellEditEnding(this, e);
|
|
}
|
|
|
|
if (AutomationPeer.ListenerExists(AutomationEvents.InvokePatternOnInvoked))
|
|
{
|
|
EGC.DataGridAutomationPeer peer = EGC.DataGridAutomationPeer.FromElement(this) as EGC.DataGridAutomationPeer;
|
|
if (peer != null)
|
|
{
|
|
peer.RaiseAutomationCellInvokeEvents(e.Column, e.Row);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void OnCanExecuteCancelEdit(object sender, CanExecuteRoutedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnCanExecuteCancelEdit(e);
|
|
}
|
|
|
|
private static void OnExecutedCancelEdit(object sender, ExecutedRoutedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnExecutedCancelEdit(e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked to determine if the CancelEdit command can be executed.
|
|
/// </summary>
|
|
protected virtual void OnCanExecuteCancelEdit(CanExecuteRoutedEventArgs e)
|
|
{
|
|
e.CanExecute = CanEndEdit(e, /* commit = */ false);
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when the CancelEdit command is executed.
|
|
/// </summary>
|
|
protected virtual void OnExecutedCancelEdit(ExecutedRoutedEventArgs e)
|
|
{
|
|
EGC.DataGridCell cell = CurrentCellContainer;
|
|
if (cell != null)
|
|
{
|
|
EGC.DataGridEditingUnit editingUnit = GetEditingUnit(e.Parameter);
|
|
|
|
bool eventCanceled = false;
|
|
if (cell.IsEditing)
|
|
{
|
|
EGC.DataGridCellEditEndingEventArgs cellEditEndingEventArgs = new EGC.DataGridCellEditEndingEventArgs(cell.Column, cell.RowOwner, cell.EditingElement, EGC.DataGridEditAction.Cancel);
|
|
OnCellEditEnding(cellEditEndingEventArgs);
|
|
|
|
eventCanceled = cellEditEndingEventArgs.Cancel;
|
|
if (!eventCanceled)
|
|
{
|
|
cell.CancelEdit();
|
|
HasCellValidationError = false;
|
|
}
|
|
}
|
|
|
|
var editableItems = EditableItems;
|
|
bool needsCommit = IsEditingItem(cell.RowDataItem) && !editableItems.CanCancelEdit;
|
|
if (!eventCanceled &&
|
|
(CanCancelAddingOrEditingRowItem(editingUnit, cell.RowDataItem) || needsCommit))
|
|
{
|
|
bool cancelAllowed = true;
|
|
|
|
if (!needsCommit)
|
|
{
|
|
EGC.DataGridRowEditEndingEventArgs rowEditEndingEventArgs = new EGC.DataGridRowEditEndingEventArgs(cell.RowOwner, EGC.DataGridEditAction.Cancel);
|
|
OnRowEditEnding(rowEditEndingEventArgs);
|
|
cancelAllowed = !rowEditEndingEventArgs.Cancel;
|
|
}
|
|
|
|
if (cancelAllowed)
|
|
{
|
|
if (needsCommit)
|
|
{
|
|
// If the row is being edited (not added), but it doesn't support
|
|
// pending changes, then tell the item to commit (this doesn't really
|
|
// do anything except update the IEditableCollectionView state).
|
|
// This allows us to exit row editing mode.
|
|
editableItems.CommitEdit();
|
|
}
|
|
else
|
|
{
|
|
CancelRowItem();
|
|
}
|
|
|
|
var bindingGroup = cell.RowOwner.BindingGroup;
|
|
if (bindingGroup != null)
|
|
{
|
|
bindingGroup.CancelEdit();
|
|
|
|
// This is to workaround the bug that BindingGroup
|
|
// does nor clear errors on CancelEdit
|
|
bindingGroup.UpdateSources();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the state of row editing
|
|
UpdateRowEditing(cell);
|
|
|
|
if (!cell.RowOwner.IsEditing)
|
|
{
|
|
// Allow the user to cancel the row and avoid being locked to that row.
|
|
// If the row is still not valid, it means that the source data is already
|
|
// invalid, and that is OK.
|
|
HasRowValidationError = false;
|
|
}
|
|
|
|
// CancelEdit and CommitEdit rely on IsAddingNewItem and IsEditingRowItem
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private static void OnCanExecuteDelete(object sender, CanExecuteRoutedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnCanExecuteDelete(e);
|
|
}
|
|
|
|
private static void OnExecutedDelete(object sender, ExecutedRoutedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnExecutedDelete(e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked to determine if the Delete command can be executed.
|
|
/// </summary>
|
|
protected virtual void OnCanExecuteDelete(CanExecuteRoutedEventArgs e)
|
|
{
|
|
e.CanExecute = CanUserDeleteRows && // User is allowed to delete
|
|
CurrentCell.IsValid && // There is a current cell
|
|
(DataItemsSelected > 0) && // There is a selection
|
|
((_currentCellContainer == null) || !_currentCellContainer.IsEditing); // Not editing a cell
|
|
e.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invoked when the Delete command is executed.
|
|
/// </summary>
|
|
protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
|
|
{
|
|
if (DataItemsSelected > 0)
|
|
{
|
|
bool shouldDelete = false;
|
|
bool isEditingRowItem = IsEditingRowItem;
|
|
if (isEditingRowItem || IsAddingNewItem)
|
|
{
|
|
// If editing or adding a row, cancel that edit.
|
|
if (CancelEdit(EGC.DataGridEditingUnit.Row) && isEditingRowItem)
|
|
{
|
|
// If adding, we're done. If editing, then an actual delete
|
|
// needs to happen.
|
|
shouldDelete = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// There is no pending edit, just delete.
|
|
shouldDelete = true;
|
|
}
|
|
|
|
if (shouldDelete)
|
|
{
|
|
// Normally, the current item will be within the selection,
|
|
// deteremine a new item to select once the items are removed.
|
|
int numSelected = SelectedItems.Count;
|
|
int indexToSelect = -1;
|
|
object currentItem = CurrentItem;
|
|
|
|
// The current item is in the selection
|
|
if (SelectedItems.Contains(currentItem))
|
|
{
|
|
// Choose the smaller index between the anchor and the current item
|
|
// as the index to select after the items are removed.
|
|
indexToSelect = Items.IndexOf(currentItem);
|
|
if (_selectionAnchor != null)
|
|
{
|
|
int anchorIndex = Items.IndexOf(_selectionAnchor.Value.Item);
|
|
if ((anchorIndex >= 0) && (anchorIndex < indexToSelect))
|
|
{
|
|
indexToSelect = anchorIndex;
|
|
}
|
|
}
|
|
|
|
indexToSelect = Math.Min(Items.Count - numSelected - 1, indexToSelect);
|
|
}
|
|
|
|
// Save off the selected items. The selected items are going to be cleared
|
|
// first as a performance optimization. When items are removed, they are checked
|
|
// against the selected items to be removed from that collection. This can be slow
|
|
// since each item could cause a linear search of the selected items collection.
|
|
// Since it is known that all of the selected items are going to be deleted, they
|
|
// can safely be unselected.
|
|
ArrayList itemsToRemove = new ArrayList(SelectedItems);
|
|
|
|
using (UpdateSelectedCells())
|
|
{
|
|
bool alreadyUpdating = IsUpdatingSelectedItems;
|
|
if (!alreadyUpdating)
|
|
{
|
|
BeginUpdateSelectedItems();
|
|
}
|
|
|
|
try
|
|
{
|
|
// Pre-emptively clear the selection lists
|
|
_selectedCells.ClearFullRows(SelectedItems);
|
|
SelectedItems.Clear();
|
|
}
|
|
finally
|
|
{
|
|
if (!alreadyUpdating)
|
|
{
|
|
EndUpdateSelectedItems();
|
|
}
|
|
}
|
|
}
|
|
|
|
// We are not going to defer the rest of the selection change due to existing
|
|
// Selector behavior. When an item is removed from the ItemsSource, the Selector
|
|
// will immediately remove it from SelectedItems. In this process, it starts a
|
|
// defer, which asserts because this code would have already started a defer.
|
|
|
|
// Remove the items that are selected
|
|
for (int i = 0; i < numSelected; i++)
|
|
{
|
|
object itemToRemove = itemsToRemove[i];
|
|
if (itemToRemove != CollectionView.NewItemPlaceholder)
|
|
{
|
|
EditableItems.Remove(itemToRemove);
|
|
}
|
|
}
|
|
|
|
// Select a new item
|
|
if (indexToSelect >= 0)
|
|
{
|
|
object itemToSelect = Items[indexToSelect];
|
|
|
|
// This should focus the row and bring it into view.
|
|
CurrentItem = itemToSelect;
|
|
|
|
// Since the current cell should be in view, there should be a container
|
|
EGC.DataGridCell cell = CurrentCellContainer;
|
|
if (cell != null)
|
|
{
|
|
_selectionAnchor = null;
|
|
HandleSelectionForCellInput(cell, /* startDragging = */ false, /* allowsExtendSelect = */ false, /* allowsMinimalSelect = */ false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Editing
|
|
|
|
/// <summary>
|
|
/// Whether the DataGrid's rows and cells can be placed in edit mode.
|
|
/// </summary>
|
|
public bool IsReadOnly
|
|
{
|
|
get { return (bool)GetValue(IsReadOnlyProperty); }
|
|
set { SetValue(IsReadOnlyProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for IsReadOnly.
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsReadOnlyProperty =
|
|
DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsReadOnlyChanged)));
|
|
|
|
private static void OnIsReadOnlyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if ((bool)e.NewValue)
|
|
{
|
|
// When going from R/W to R/O, cancel any current edits
|
|
((EGC.DataGrid)d).CancelAnyEdit();
|
|
}
|
|
|
|
// re-evalutate the BeginEdit command's CanExecute.
|
|
CommandManager.InvalidateRequerySuggested();
|
|
|
|
d.CoerceValue(CanUserAddRowsProperty);
|
|
d.CoerceValue(CanUserDeleteRowsProperty);
|
|
|
|
// Affects the IsReadOnly property on cells
|
|
OnNotifyColumnAndCellPropertyChanged(d, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The object (or row) that, if not in edit mode, can be edited.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is the data item for the row that either has or contains focus.
|
|
/// </remarks>
|
|
public object CurrentItem
|
|
{
|
|
get { return (object)GetValue(CurrentItemProperty); }
|
|
set { SetValue(CurrentItemProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for CurrentItem.
|
|
/// </summary>
|
|
public static readonly DependencyProperty CurrentItemProperty =
|
|
DependencyProperty.Register("CurrentItem", typeof(object), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnCurrentItemChanged)));
|
|
|
|
private static void OnCurrentItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
EGC.DataGridCellInfo currentCell = dataGrid.CurrentCell;
|
|
object newItem = e.NewValue;
|
|
|
|
if (currentCell.Item != newItem)
|
|
{
|
|
// Update the CurrentCell structure with the new item
|
|
dataGrid.CurrentCell = EGC.DataGridCellInfo.CreatePossiblyPartialCellInfo(newItem, currentCell.Column, dataGrid);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The column of the CurrentItem (row) that corresponds with the current cell.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// null indicates that a cell does not have focus. The row may still have focus.
|
|
/// </remarks>
|
|
public EGC.DataGridColumn CurrentColumn
|
|
{
|
|
get { return (EGC.DataGridColumn)GetValue(CurrentColumnProperty); }
|
|
set { SetValue(CurrentColumnProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for CurrentColumn.
|
|
/// </summary>
|
|
public static readonly DependencyProperty CurrentColumnProperty =
|
|
DependencyProperty.Register("CurrentColumn", typeof(EGC.DataGridColumn), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnCurrentColumnChanged)));
|
|
|
|
private static void OnCurrentColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
EGC.DataGridCellInfo currentCell = dataGrid.CurrentCell;
|
|
EGC.DataGridColumn newColumn = (EGC.DataGridColumn)e.NewValue;
|
|
|
|
if (currentCell.Column != newColumn)
|
|
{
|
|
// Update the CurrentCell structure with the new column
|
|
dataGrid.CurrentCell = EGC.DataGridCellInfo.CreatePossiblyPartialCellInfo(currentCell.Item, newColumn, dataGrid);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cell that, if not in edit mode, can be edited.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The value returned is a structure that provides enough information to describe
|
|
/// the cell. It is neither an actual reference to the cell container nor the value
|
|
/// displayed in a given cell.
|
|
/// </remarks>
|
|
public EGC.DataGridCellInfo CurrentCell
|
|
{
|
|
get { return (EGC.DataGridCellInfo)GetValue(CurrentCellProperty); }
|
|
set { SetValue(CurrentCellProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for CurrentCell.
|
|
/// </summary>
|
|
public static readonly DependencyProperty CurrentCellProperty =
|
|
DependencyProperty.Register("CurrentCell", typeof(EGC.DataGridCellInfo), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridCellInfo.Unset, new PropertyChangedCallback(OnCurrentCellChanged)));
|
|
|
|
private static void OnCurrentCellChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
EGC.DataGridCellInfo oldCell = (EGC.DataGridCellInfo)e.OldValue;
|
|
EGC.DataGridCellInfo currentCell = (EGC.DataGridCellInfo)e.NewValue;
|
|
|
|
if (dataGrid.CurrentItem != currentCell.Item)
|
|
{
|
|
dataGrid.CurrentItem = currentCell.Item;
|
|
}
|
|
|
|
if (dataGrid.CurrentColumn != currentCell.Column)
|
|
{
|
|
dataGrid.CurrentColumn = currentCell.Column;
|
|
}
|
|
|
|
if (dataGrid._currentCellContainer != null)
|
|
{
|
|
// _currentCellContainer should still be the old container and not the new one.
|
|
// If _currentCellContainer were null, then it should mean that no BeginEdit was called
|
|
// so, we shouldn't be missing any EndEdits.
|
|
if ((dataGrid.IsAddingNewItem || dataGrid.IsEditingRowItem) && (oldCell.Item != currentCell.Item))
|
|
{
|
|
// There is a row edit pending and the current cell changed to another row.
|
|
// Commit the row, which also commits the cell.
|
|
dataGrid.EndEdit(CommitEditCommand, dataGrid._currentCellContainer, EGC.DataGridEditingUnit.Row, /* exitEditingMode = */ true);
|
|
}
|
|
else if (dataGrid._currentCellContainer.IsEditing)
|
|
{
|
|
// Only the cell needs to commit.
|
|
dataGrid.EndEdit(CommitEditCommand, dataGrid._currentCellContainer, EGC.DataGridEditingUnit.Cell, /* exitEditingMode = */ true);
|
|
}
|
|
}
|
|
|
|
dataGrid._currentCellContainer = null;
|
|
|
|
if (currentCell.IsValid && dataGrid.IsKeyboardFocusWithin)
|
|
{
|
|
// If CurrentCell was set by the user and not through a focus change,
|
|
// then focus must be updated, but only when the DataGrid already
|
|
// has focus.
|
|
EGC.DataGridCell cell = dataGrid._pendingCurrentCellContainer;
|
|
if (cell == null)
|
|
{
|
|
cell = dataGrid.CurrentCellContainer;
|
|
if (cell == null)
|
|
{
|
|
// The cell might be virtualized. Try to devirtualize by scrolling.
|
|
dataGrid.ScrollCellIntoView(currentCell.Item, currentCell.Column);
|
|
cell = dataGrid.CurrentCellContainer;
|
|
}
|
|
}
|
|
|
|
if ((cell != null) && !cell.IsKeyboardFocusWithin)
|
|
{
|
|
cell.Focus();
|
|
}
|
|
}
|
|
|
|
dataGrid.OnCurrentCellChanged(EventArgs.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An event to notify that the value of CurrentCell changed.
|
|
/// </summary>
|
|
public event EventHandler<EventArgs> CurrentCellChanged;
|
|
|
|
/// <summary>
|
|
/// Called when the value of CurrentCell changes.
|
|
/// </summary>
|
|
/// <param name="e">Empty event arguments.</param>
|
|
protected virtual void OnCurrentCellChanged(EventArgs e)
|
|
{
|
|
if (CurrentCellChanged != null)
|
|
{
|
|
CurrentCellChanged(this, e);
|
|
}
|
|
}
|
|
|
|
private void UpdateCurrentCell(EGC.DataGridCell cell, bool isKeyboardFocusWithin)
|
|
{
|
|
if (isKeyboardFocusWithin)
|
|
{
|
|
// Focus is within the cell, make it the current cell.
|
|
CurrentCellContainer = cell;
|
|
}
|
|
else if (!IsKeyboardFocusWithin)
|
|
{
|
|
// Focus moved outside the DataGrid, so clear out the current cell.
|
|
CurrentCellContainer = null;
|
|
}
|
|
|
|
// Focus is within the DataGrid but not within this particular cell.
|
|
// Assume that focus is moving to another cell, and that cell will update
|
|
// the current cell.
|
|
}
|
|
|
|
private EGC.DataGridCell CurrentCellContainer
|
|
{
|
|
get
|
|
{
|
|
if (_currentCellContainer == null)
|
|
{
|
|
EGC.DataGridCellInfo currentCell = CurrentCell;
|
|
if (currentCell.IsValid)
|
|
{
|
|
_currentCellContainer = TryFindCell(currentCell);
|
|
}
|
|
}
|
|
|
|
return _currentCellContainer;
|
|
}
|
|
|
|
set
|
|
{
|
|
if ((_currentCellContainer != value) &&
|
|
((value == null) || (value != _pendingCurrentCellContainer)))
|
|
{
|
|
// Setting CurrentCell might cause some re-entrancy due to focus changes.
|
|
// We need to detect this without actually changing the value until after
|
|
// setting CurrentCell.
|
|
_pendingCurrentCellContainer = value;
|
|
|
|
// _currentCellContainer must remain intact while changing CurrentCell
|
|
// so that the previous edit can be committed.
|
|
if (value == null)
|
|
{
|
|
ClearValue(CurrentCellProperty);
|
|
}
|
|
else
|
|
{
|
|
CurrentCell = new EGC.DataGridCellInfo(value);
|
|
}
|
|
|
|
_pendingCurrentCellContainer = null;
|
|
_currentCellContainer = value;
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsEditingCurrentCell
|
|
{
|
|
get
|
|
{
|
|
EGC.DataGridCell cell = CurrentCellContainer;
|
|
if (cell != null)
|
|
{
|
|
return cell.IsEditing;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool IsCurrentCellReadOnly
|
|
{
|
|
get
|
|
{
|
|
EGC.DataGridCell cell = CurrentCellContainer;
|
|
if (cell != null)
|
|
{
|
|
return cell.IsReadOnly;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called just before a cell will change to edit mode
|
|
/// to allow handlers to prevent the cell from entering edit mode.
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridBeginningEditEventArgs> BeginningEdit;
|
|
|
|
/// <summary>
|
|
/// Called just before a cell will change to edit mode
|
|
/// to all subclasses to prevent the cell from entering edit mode.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Default implementation raises the BeginningEdit event.
|
|
/// </remarks>
|
|
protected virtual void OnBeginningEdit(EGC.DataGridBeginningEditEventArgs e)
|
|
{
|
|
if (BeginningEdit != null)
|
|
{
|
|
BeginningEdit(this, e);
|
|
}
|
|
|
|
if (AutomationPeer.ListenerExists(AutomationEvents.InvokePatternOnInvoked))
|
|
{
|
|
EGC.DataGridAutomationPeer peer = EGC.DataGridAutomationPeer.FromElement(this) as EGC.DataGridAutomationPeer;
|
|
if (peer != null)
|
|
{
|
|
peer.RaiseAutomationCellInvokeEvents(e.Column, e.Row);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after a cell has changed to editing mode to allow
|
|
/// handlers to modify the contents of the cell.
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridPreparingCellForEditEventArgs> PreparingCellForEdit;
|
|
|
|
/// <summary>
|
|
/// Called after a cell has changed to editing mode to allow
|
|
/// subclasses to modify the contents of the cell.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Default implementation raises the PreparingCellForEdit event.
|
|
/// This method is invoked from DataGridCell (instead of DataGrid) once it has entered edit mode.
|
|
/// </remarks>
|
|
protected internal virtual void OnPreparingCellForEdit(EGC.DataGridPreparingCellForEditEventArgs e)
|
|
{
|
|
if (PreparingCellForEdit != null)
|
|
{
|
|
PreparingCellForEdit(this, e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the BeginEdit command, which will place the current cell or row into
|
|
/// edit mode.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the command is enabled, this will Strip to the BeginningEdit and PreparingCellForEdit
|
|
/// overrides and events.
|
|
/// </remarks>
|
|
/// <returns>true if the current cell or row enters edit mode, false otherwise.</returns>
|
|
public bool BeginEdit()
|
|
{
|
|
return BeginEdit(/* editingEventArgs = */ null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the BeginEdit command, which will place the current cell or row into
|
|
/// edit mode.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the command is enabled, this will Strip to the BeginningEdit and PreparingCellForEdit
|
|
/// overrides and events.
|
|
/// </remarks>
|
|
/// <param name="editingEventArgs">The event arguments, if any, that led to BeginEdit being called. May be null.</param>
|
|
/// <returns>true if the current cell or row enters edit mode, false otherwise.</returns>
|
|
public bool BeginEdit(RoutedEventArgs editingEventArgs)
|
|
{
|
|
if (!IsReadOnly)
|
|
{
|
|
EGC.DataGridCell cellContainer = CurrentCellContainer;
|
|
if (cellContainer != null)
|
|
{
|
|
if (!cellContainer.IsEditing &&
|
|
BeginEditCommand.CanExecute(editingEventArgs, cellContainer))
|
|
{
|
|
BeginEditCommand.Execute(editingEventArgs, cellContainer);
|
|
}
|
|
|
|
return cellContainer.IsEditing;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the CancelEdit command.
|
|
/// If a cell is currently in edit mode, cancels the cell edit, but leaves any row edits alone.
|
|
/// If a cell is not in edit mode, then cancels any pending row edits.
|
|
/// </summary>
|
|
/// <returns>true if the current cell or row exits edit mode, false otherwise.</returns>
|
|
public bool CancelEdit()
|
|
{
|
|
if (IsEditingCurrentCell)
|
|
{
|
|
return CancelEdit(EGC.DataGridEditingUnit.Cell);
|
|
}
|
|
else if (IsEditingRowItem || IsAddingNewItem)
|
|
{
|
|
return CancelEdit(EGC.DataGridEditingUnit.Row);
|
|
}
|
|
|
|
return true; // No one is in edit mode
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the CancelEdit command.
|
|
/// If a cell is currently in edit mode, cancels the cell edit, but leaves any row edits alone.
|
|
/// </summary>
|
|
/// <returns>true if the cell exits edit mode, false otherwise.</returns>
|
|
internal bool CancelEdit(EGC.DataGridCell cell)
|
|
{
|
|
EGC.DataGridCell currentCell = CurrentCellContainer;
|
|
if (currentCell != null && currentCell == cell && currentCell.IsEditing)
|
|
{
|
|
return CancelEdit(EGC.DataGridEditingUnit.Cell);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the CancelEdit command.
|
|
/// Reverts any pending editing changes to the desired editing unit and exits edit mode.
|
|
/// </summary>
|
|
/// <param name="editingUnit">Whether to cancel edit mode of the current cell or current row.</param>
|
|
/// <returns>true if the current cell or row exits edit mode, false otherwise.</returns>
|
|
public bool CancelEdit(EGC.DataGridEditingUnit editingUnit)
|
|
{
|
|
return EndEdit(CancelEditCommand, CurrentCellContainer, editingUnit, true);
|
|
}
|
|
|
|
private void CancelAnyEdit()
|
|
{
|
|
if (IsAddingNewItem || IsEditingRowItem)
|
|
{
|
|
// There is a row edit in progress, cancel it, which will also cancel the cell edit.
|
|
CancelEdit(EGC.DataGridEditingUnit.Row);
|
|
}
|
|
else if (IsEditingCurrentCell)
|
|
{
|
|
// Cancel the current cell edit.
|
|
CancelEdit(EGC.DataGridEditingUnit.Cell);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the CommitEdit command.
|
|
/// If a cell is currently being edited, commits any pending changes to the cell, but
|
|
/// leaves any pending changes to the row. This should mean that changes are propagated
|
|
/// from the editing environment to the pending row.
|
|
/// If a cell is not currently being edited, then commits any pending rows.
|
|
/// </summary>
|
|
/// <returns>true if the current cell or row exits edit mode, false otherwise.</returns>
|
|
public bool CommitEdit()
|
|
{
|
|
if (IsEditingCurrentCell)
|
|
{
|
|
return CommitEdit(EGC.DataGridEditingUnit.Cell, true);
|
|
}
|
|
else if (IsEditingRowItem || IsAddingNewItem)
|
|
{
|
|
return CommitEdit(EGC.DataGridEditingUnit.Row, true);
|
|
}
|
|
|
|
return true; // No one is in edit mode
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the CommitEdit command.
|
|
/// Commits any pending changes for the given editing unit and exits edit mode.
|
|
/// </summary>
|
|
/// <param name="editingUnit">Whether to commit changes for the current cell or current row.</param>
|
|
/// <param name="exitEditingMode">Whether to exit edit mode.</param>
|
|
/// <returns>true if the current cell or row exits edit mode, false otherwise.</returns>
|
|
public bool CommitEdit(EGC.DataGridEditingUnit editingUnit, bool exitEditingMode)
|
|
{
|
|
return EndEdit(CommitEditCommand, CurrentCellContainer, editingUnit, exitEditingMode);
|
|
}
|
|
|
|
private bool CommitAnyEdit()
|
|
{
|
|
if (IsAddingNewItem || IsEditingRowItem)
|
|
{
|
|
// There is a row edit in progress, commit it, which will also commit the cell edit.
|
|
return CommitEdit(EGC.DataGridEditingUnit.Row, /* exitEditingMode = */ true);
|
|
}
|
|
else if (IsEditingCurrentCell)
|
|
{
|
|
// Commit the current cell edit.
|
|
return CommitEdit(EGC.DataGridEditingUnit.Cell, /* exitEditingMode = */ true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool EndEdit(RoutedCommand command, EGC.DataGridCell cellContainer, EGC.DataGridEditingUnit editingUnit, bool exitEditMode)
|
|
{
|
|
bool cellLeftEditingMode = true;
|
|
bool rowLeftEditingMode = true;
|
|
|
|
if (cellContainer != null)
|
|
{
|
|
if (command.CanExecute(editingUnit, cellContainer))
|
|
{
|
|
command.Execute(editingUnit, cellContainer);
|
|
}
|
|
|
|
cellLeftEditingMode = !cellContainer.IsEditing;
|
|
rowLeftEditingMode = !IsEditingRowItem && !IsAddingNewItem;
|
|
}
|
|
|
|
if (!exitEditMode)
|
|
{
|
|
if (editingUnit == EGC.DataGridEditingUnit.Cell)
|
|
{
|
|
if (cellContainer != null)
|
|
{
|
|
if (cellLeftEditingMode)
|
|
{
|
|
return BeginEdit(null);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// A cell was not placed in edit mode
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (rowLeftEditingMode)
|
|
{
|
|
object rowItem = cellContainer.RowDataItem;
|
|
if (rowItem != null)
|
|
{
|
|
EditRowItem(rowItem);
|
|
return IsEditingRowItem;
|
|
}
|
|
}
|
|
|
|
// A row item was not placed in edit mode
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return cellLeftEditingMode && ((editingUnit == EGC.DataGridEditingUnit.Cell) || rowLeftEditingMode);
|
|
}
|
|
|
|
private bool HasCellValidationError
|
|
{
|
|
get
|
|
{
|
|
return _hasCellValidationError;
|
|
}
|
|
|
|
set
|
|
{
|
|
if (_hasCellValidationError != value)
|
|
{
|
|
_hasCellValidationError = value;
|
|
|
|
// BeginEdit's CanExecute status relies on this flag
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool HasRowValidationError
|
|
{
|
|
get
|
|
{
|
|
return _hasRowValidationError;
|
|
}
|
|
|
|
set
|
|
{
|
|
if (_hasRowValidationError != value)
|
|
{
|
|
_hasRowValidationError = value;
|
|
|
|
// BeginEdit's CanExecute status relies on this flag
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Row Editing
|
|
|
|
/// <summary>
|
|
/// Whether the end-user can add new rows to the ItemsSource.
|
|
/// </summary>
|
|
public bool CanUserAddRows
|
|
{
|
|
get { return (bool)GetValue(CanUserAddRowsProperty); }
|
|
set { SetValue(CanUserAddRowsProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for CanUserAddRows.
|
|
/// </summary>
|
|
public static readonly DependencyProperty CanUserAddRowsProperty =
|
|
DependencyProperty.Register("CanUserAddRows", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnCanUserAddRowsChanged), new CoerceValueCallback(OnCoerceCanUserAddRows)));
|
|
|
|
private static void OnCanUserAddRowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).UpdateNewItemPlaceholder(/* isAddingNewItem = */ false);
|
|
}
|
|
|
|
private static object OnCoerceCanUserAddRows(DependencyObject d, object baseValue)
|
|
{
|
|
return OnCoerceCanUserAddOrDeleteRows((EGC.DataGrid)d, (bool)baseValue, /* canUserAddRowsProperty = */ true);
|
|
}
|
|
|
|
private static bool OnCoerceCanUserAddOrDeleteRows(EGC.DataGrid dataGrid, bool baseValue, bool canUserAddRowsProperty)
|
|
{
|
|
// Only when the base value is true do we need to validate that the user
|
|
// can actually add or delete rows.
|
|
if (baseValue)
|
|
{
|
|
if (dataGrid.IsReadOnly || !dataGrid.IsEnabled)
|
|
{
|
|
// Read-only/disabled DataGrids cannot be modified.
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if ((canUserAddRowsProperty && !dataGrid.EditableItems.CanAddNew) ||
|
|
(!canUserAddRowsProperty && !dataGrid.EditableItems.CanRemove))
|
|
{
|
|
// The collection view does not allow the add or delete action
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the end-user can delete rows from the ItemsSource.
|
|
/// </summary>
|
|
public bool CanUserDeleteRows
|
|
{
|
|
get { return (bool)GetValue(CanUserDeleteRowsProperty); }
|
|
set { SetValue(CanUserDeleteRowsProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for CanUserDeleteRows.
|
|
/// </summary>
|
|
public static readonly DependencyProperty CanUserDeleteRowsProperty =
|
|
DependencyProperty.Register("CanUserDeleteRows", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnCanUserDeleteRowsChanged), new CoerceValueCallback(OnCoerceCanUserDeleteRows)));
|
|
|
|
private static void OnCanUserDeleteRowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
// The Delete command needs to have CanExecute run
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
|
|
private static object OnCoerceCanUserDeleteRows(DependencyObject d, object baseValue)
|
|
{
|
|
return OnCoerceCanUserAddOrDeleteRows((EGC.DataGrid)d, (bool)baseValue, /* canUserAddRowsProperty = */ false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An event that is raised when a new item is created so that
|
|
/// developers can initialize the item with custom default values.
|
|
/// </summary>
|
|
public event EGC.InitializingNewItemEventHandler InitializingNewItem;
|
|
|
|
/// <summary>
|
|
/// A method that is called when a new item is created so that
|
|
/// overrides can initialize the item with custom default values.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The default implementation raises the InitializingNewItem event.
|
|
/// </remarks>
|
|
/// <param name="e">Event arguments that provide access to the new item.</param>
|
|
protected virtual void OnInitializingNewItem(EGC.InitializingNewItemEventArgs e)
|
|
{
|
|
if (InitializingNewItem != null)
|
|
{
|
|
InitializingNewItem(this, e);
|
|
}
|
|
}
|
|
|
|
private object AddNewItem()
|
|
{
|
|
Debug.Assert(CanUserAddRows, "AddNewItem called when the end-user cannot add new rows.");
|
|
Debug.Assert(!IsAddingNewItem, "AddNewItem called when a pending add is taking place.");
|
|
|
|
// Hide the placeholder
|
|
UpdateNewItemPlaceholder(/* isAddingNewItem = */ true);
|
|
|
|
object newItem = EditableItems.AddNew();
|
|
if (newItem != null)
|
|
{
|
|
EGC.InitializingNewItemEventArgs e = new EGC.InitializingNewItemEventArgs(newItem);
|
|
OnInitializingNewItem(e);
|
|
}
|
|
|
|
// CancelEdit and CommitEdit rely on IsAddingNewItem
|
|
CommandManager.InvalidateRequerySuggested();
|
|
|
|
return newItem;
|
|
}
|
|
|
|
private void EditRowItem(object rowItem)
|
|
{
|
|
EditableItems.EditItem(rowItem);
|
|
|
|
// CancelEdit and CommitEdit rely on IsEditingRowItem
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
|
|
private void CommitRowItem()
|
|
{
|
|
// This case illustrates a minor side-effect of communicating with IEditableObject through two different means
|
|
// - BindingGroup
|
|
// - IEditableCollectionView
|
|
// The sequence of operations is as below.
|
|
// IEditableCollectionView.BeginEdit
|
|
// BindingGroup.BeginEdit
|
|
// BindingGroup.CommitEdit
|
|
// IEditableCollectionView.CommitEdit
|
|
// After having commited the NewItem row the first time it is possible that the IsAddingNewItem is false
|
|
// during the second call to CommitEdit. Hence we cannot quite make this assertion here.
|
|
|
|
// Debug.Assert(IsEditingRowItem || IsAddingNewItem, "CommitRowItem was called when a row was not being edited or added.");
|
|
if (IsEditingRowItem)
|
|
{
|
|
EditableItems.CommitEdit();
|
|
}
|
|
else
|
|
{
|
|
EditableItems.CommitNew();
|
|
|
|
// Show the placeholder again
|
|
UpdateNewItemPlaceholder(/* isAddingNewItem = */ false);
|
|
}
|
|
}
|
|
|
|
private void CancelRowItem()
|
|
{
|
|
// This case illustrates a minor side-effect of communicating with IEditableObject through two different means
|
|
// - BindingGroup
|
|
// - IEditableCollectionView
|
|
// The sequence of operations is as below.
|
|
// IEditableCollectionView.BeginEdit
|
|
// BindingGroup.BeginEdit
|
|
// IEditableCollectionView.CancelEdit
|
|
// BindingGroup.CancelEdit
|
|
// After having cancelled the NewItem row the first time it is possible that the IsAddingNewItem is false
|
|
// during the second call to CancelEdit. Hence we cannot quite make this assertion here.
|
|
|
|
// Debug.Assert(IsEditingRowItem || IsAddingNewItem, "CancelRowItem was called when a row was not being edited or added.");
|
|
if (IsEditingRowItem)
|
|
{
|
|
EditableItems.CancelEdit();
|
|
}
|
|
else
|
|
{
|
|
object currentAddItem = EditableItems.CurrentAddItem;
|
|
bool wasCurrent = currentAddItem == CurrentItem;
|
|
bool wasSelected = SelectedItems.Contains(currentAddItem);
|
|
bool reselectPlaceholderCells = false;
|
|
List<int> columnIndexRanges = null;
|
|
int newItemIndex = -1;
|
|
|
|
if (wasSelected)
|
|
{
|
|
// Unselect the item that was being added
|
|
UnselectItem(currentAddItem);
|
|
}
|
|
else
|
|
{
|
|
// Cells will automatically unselect when the new item is removed, but we
|
|
// should reselect them on the placeholder.
|
|
newItemIndex = Items.IndexOf(currentAddItem);
|
|
reselectPlaceholderCells = ((newItemIndex >= 0) && _selectedCells.Intersects(newItemIndex, out columnIndexRanges));
|
|
}
|
|
|
|
// Cancel the add and remove it from the collection
|
|
EditableItems.CancelNew();
|
|
|
|
// Show the placeholder again
|
|
UpdateNewItemPlaceholder(/* isAddingNewItem = */ false);
|
|
|
|
if (wasCurrent)
|
|
{
|
|
// Focus the placeholder if the new item had focus
|
|
CurrentItem = CollectionView.NewItemPlaceholder;
|
|
}
|
|
|
|
if (wasSelected)
|
|
{
|
|
// Re-select the placeholder if it was selected before
|
|
SelectItem(CollectionView.NewItemPlaceholder);
|
|
}
|
|
else if (reselectPlaceholderCells)
|
|
{
|
|
// Re-select placeholder cells if they were selected before
|
|
using (UpdateSelectedCells())
|
|
{
|
|
int rowIndex = newItemIndex;
|
|
bool placeholderAtBeginning = (EditableItems.NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning);
|
|
|
|
// When the placeholder is at the beginning, we need to unselect the cells
|
|
// in the added row and move those back to the previous row.
|
|
if (placeholderAtBeginning)
|
|
{
|
|
_selectedCells.RemoveRegion(newItemIndex, 0, 1, Columns.Count);
|
|
rowIndex--;
|
|
}
|
|
|
|
for (int i = 0, count = columnIndexRanges.Count; i < count; i += 2)
|
|
{
|
|
_selectedCells.AddRegion(rowIndex, columnIndexRanges[i], 1, columnIndexRanges[i + 1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateRowEditing(EGC.DataGridCell cell)
|
|
{
|
|
object rowDataItem = cell.RowDataItem;
|
|
|
|
// If the row is not in edit/add mode, then clear its IsEditing flag.
|
|
if (!IsAddingOrEditingRowItem(rowDataItem))
|
|
{
|
|
cell.RowOwner.IsEditing = false;
|
|
_editingRowItem = null;
|
|
_editingRowIndex = -1;
|
|
}
|
|
}
|
|
|
|
private IEditableCollectionView EditableItems
|
|
{
|
|
get { return (IEditableCollectionView)Items; }
|
|
}
|
|
|
|
private bool IsAddingNewItem
|
|
{
|
|
get { return EditableItems.IsAddingNew; }
|
|
}
|
|
|
|
private bool IsEditingRowItem
|
|
{
|
|
get { return EditableItems.IsEditingItem; }
|
|
}
|
|
|
|
private bool IsAddingOrEditingRowItem(object item)
|
|
{
|
|
return IsEditingItem(item) ||
|
|
(IsAddingNewItem && (EditableItems.CurrentAddItem == item));
|
|
}
|
|
|
|
private bool CanCancelAddingOrEditingRowItem(EGC.DataGridEditingUnit editingUnit, object item)
|
|
{
|
|
return (editingUnit == EGC.DataGridEditingUnit.Row) &&
|
|
((IsEditingItem(item) && EditableItems.CanCancelEdit) ||
|
|
(IsAddingNewItem && (EditableItems.CurrentAddItem == item)));
|
|
}
|
|
|
|
private bool IsEditingItem(object item)
|
|
{
|
|
return IsEditingRowItem && (EditableItems.CurrentEditItem == item);
|
|
}
|
|
|
|
private void UpdateNewItemPlaceholder(bool isAddingNewItem)
|
|
{
|
|
var editableItems = EditableItems;
|
|
bool canUserAddRows = CanUserAddRows;
|
|
|
|
if (EGC.DataGridHelper.IsDefaultValue(this, CanUserAddRowsProperty))
|
|
{
|
|
canUserAddRows = OnCoerceCanUserAddOrDeleteRows(this, canUserAddRows, true);
|
|
}
|
|
|
|
if (!isAddingNewItem)
|
|
{
|
|
if (canUserAddRows)
|
|
{
|
|
// NewItemPlaceholderPosition isn't a DP but we want to default to AtEnd instead of None (can only be done
|
|
// when canUserAddRows becomes true). This may override the users intent to make it None, however
|
|
// they can work around this by resetting it to None after making a change which results in canUserAddRows
|
|
// becoming true.
|
|
if (editableItems.NewItemPlaceholderPosition == NewItemPlaceholderPosition.None)
|
|
{
|
|
editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtEnd;
|
|
}
|
|
|
|
_placeholderVisibility = Visibility.Visible;
|
|
}
|
|
else
|
|
{
|
|
if (editableItems.NewItemPlaceholderPosition != NewItemPlaceholderPosition.None)
|
|
{
|
|
editableItems.NewItemPlaceholderPosition = NewItemPlaceholderPosition.None;
|
|
}
|
|
|
|
_placeholderVisibility = Visibility.Collapsed;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// During a row add, hide the placeholder
|
|
_placeholderVisibility = Visibility.Collapsed;
|
|
}
|
|
|
|
// Make sure the newItemPlaceholderRow reflects the correct visiblity
|
|
EGC.DataGridRow newItemPlaceholderRow = (EGC.DataGridRow)ItemContainerGenerator.ContainerFromItem(CollectionView.NewItemPlaceholder);
|
|
if (newItemPlaceholderRow != null)
|
|
{
|
|
newItemPlaceholderRow.CoerceValue(VisibilityProperty);
|
|
}
|
|
}
|
|
|
|
private void SetCurrentItemToPlaceholder()
|
|
{
|
|
NewItemPlaceholderPosition position = EditableItems.NewItemPlaceholderPosition;
|
|
if (position == NewItemPlaceholderPosition.AtEnd)
|
|
{
|
|
int itemCount = Items.Count;
|
|
if (itemCount > 0)
|
|
{
|
|
CurrentItem = Items[itemCount - 1];
|
|
}
|
|
}
|
|
else if (position == NewItemPlaceholderPosition.AtBeginning)
|
|
{
|
|
if (Items.Count > 0)
|
|
{
|
|
CurrentItem = Items[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
private int DataItemsCount
|
|
{
|
|
get
|
|
{
|
|
int itemsCount = Items.Count;
|
|
|
|
// Subtract one if there is a new item placeholder
|
|
if (HasNewItemPlaceholder)
|
|
{
|
|
itemsCount--;
|
|
}
|
|
|
|
return itemsCount;
|
|
}
|
|
}
|
|
|
|
private int DataItemsSelected
|
|
{
|
|
get
|
|
{
|
|
int itemsSelected = SelectedItems.Count;
|
|
|
|
if (HasNewItemPlaceholder && SelectedItems.Contains(CollectionView.NewItemPlaceholder))
|
|
{
|
|
itemsSelected--;
|
|
}
|
|
|
|
return itemsSelected;
|
|
}
|
|
}
|
|
|
|
private bool HasNewItemPlaceholder
|
|
{
|
|
get
|
|
{
|
|
IEditableCollectionView editableItems = EditableItems;
|
|
return editableItems.NewItemPlaceholderPosition != NewItemPlaceholderPosition.None;
|
|
}
|
|
}
|
|
|
|
private bool IsNewItemPlaceholder(object item)
|
|
{
|
|
return (item == CollectionView.NewItemPlaceholder) || (item == EGC.DataGrid.NewItemPlaceholder);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Row Details
|
|
|
|
/// <summary>
|
|
/// Determines which visibility mode the Row's details use.
|
|
/// </summary>
|
|
public EGC.DataGridRowDetailsVisibilityMode RowDetailsVisibilityMode
|
|
{
|
|
get { return (EGC.DataGridRowDetailsVisibilityMode)GetValue(RowDetailsVisibilityModeProperty); }
|
|
set { SetValue(RowDetailsVisibilityModeProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for RowDetailsVisibilityMode.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowDetailsVisibilityModeProperty =
|
|
DependencyProperty.Register("RowDetailsVisibilityMode", typeof(EGC.DataGridRowDetailsVisibilityMode), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridRowDetailsVisibilityMode.VisibleWhenSelected, OnNotifyRowAndDetailsPropertyChanged));
|
|
|
|
/// <summary>
|
|
/// Controls if the row details scroll.
|
|
/// </summary>
|
|
public bool AreRowDetailsFrozen
|
|
{
|
|
get { return (bool)GetValue(AreRowDetailsFrozenProperty); }
|
|
set { SetValue(AreRowDetailsFrozenProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for AreRowDetailsFrozen.
|
|
/// </summary>
|
|
public static readonly DependencyProperty AreRowDetailsFrozenProperty =
|
|
DependencyProperty.Register("AreRowDetailsFrozen", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// Template used for the Row details.
|
|
/// </summary>
|
|
public DataTemplate RowDetailsTemplate
|
|
{
|
|
get { return (DataTemplate)GetValue(RowDetailsTemplateProperty); }
|
|
set { SetValue(RowDetailsTemplateProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for RowDetailsTemplate.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowDetailsTemplateProperty =
|
|
DependencyProperty.Register("RowDetailsTemplate", typeof(DataTemplate), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, OnNotifyRowAndDetailsPropertyChanged));
|
|
|
|
/// <summary>
|
|
/// TemplateSelector used for the Row details
|
|
/// </summary>
|
|
public DataTemplateSelector RowDetailsTemplateSelector
|
|
{
|
|
get { return (DataTemplateSelector)GetValue(RowDetailsTemplateSelectorProperty); }
|
|
set { SetValue(RowDetailsTemplateSelectorProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DependencyProperty for RowDetailsTemplateSelector.
|
|
/// </summary>
|
|
public static readonly DependencyProperty RowDetailsTemplateSelectorProperty =
|
|
DependencyProperty.Register("RowDetailsTemplateSelector", typeof(DataTemplateSelector), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, OnNotifyRowAndDetailsPropertyChanged));
|
|
|
|
/// <summary>
|
|
/// Event that is fired just before the details of a Row is shown
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridRowDetailsEventArgs> LoadingRowDetails;
|
|
|
|
/// <summary>
|
|
/// Event that is fired just before the details of a Row is hidden
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridRowDetailsEventArgs> UnloadingRowDetails;
|
|
|
|
/// <summary>
|
|
/// Event that is fired when the visibility of a Rows details changes.
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridRowDetailsEventArgs> RowDetailsVisibilityChanged;
|
|
|
|
/// <summary>
|
|
/// Invokes the LoadingRowDetails event
|
|
/// </summary>
|
|
protected virtual void OnLoadingRowDetails(EGC.DataGridRowDetailsEventArgs e)
|
|
{
|
|
e.Row.DetailsEventStatus = EGC.DataGridRow.RowDetailsEventStatus.Loaded;
|
|
if (LoadingRowDetails != null)
|
|
{
|
|
LoadingRowDetails(this, e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the UnloadingRowDetails event
|
|
/// </summary>
|
|
protected virtual void OnUnloadingRowDetails(EGC.DataGridRowDetailsEventArgs e)
|
|
{
|
|
if (UnloadingRowDetails != null)
|
|
{
|
|
UnloadingRowDetails(this, e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes the RowDetailsVisibilityChanged event
|
|
/// </summary>
|
|
protected internal virtual void OnRowDetailsVisibilityChanged(EGC.DataGridRowDetailsEventArgs e)
|
|
{
|
|
if (RowDetailsVisibilityChanged != null)
|
|
{
|
|
RowDetailsVisibilityChanged(this, e);
|
|
}
|
|
|
|
var row = e.Row;
|
|
|
|
// Don't fire LoadingRowDetails it's disabled or already loaded.
|
|
if (row.DetailsEventStatus == EGC.DataGridRow.RowDetailsEventStatus.Pending && row.DetailsVisibility == Visibility.Visible && row.DetailsPresenter != null)
|
|
{
|
|
// No need to used DelayedOnLoadingRowDetails because OnRowDetailsVisibilityChanged isn't called until after the
|
|
// template is expanded.
|
|
OnLoadingRowDetails(new EGC.DataGridRowDetailsEventArgs(row, row.DetailsPresenter.DetailsElement));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Row Resizing
|
|
|
|
/// <summary>
|
|
/// A property that specifies whether the user can resize rows in the UI by dragging the row headers.
|
|
/// </summary>
|
|
public bool CanUserResizeRows
|
|
{
|
|
get { return (bool)GetValue(CanUserResizeRowsProperty); }
|
|
set { SetValue(CanUserResizeRowsProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the CanUserResizeColumns property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty CanUserResizeRowsProperty =
|
|
DependencyProperty.Register("CanUserResizeRows", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnNotifyRowHeaderPropertyChanged)));
|
|
|
|
#endregion
|
|
|
|
#region Selection
|
|
|
|
/// <summary>
|
|
/// The currently selected cells.
|
|
/// </summary>
|
|
public IList<EGC.DataGridCellInfo> SelectedCells
|
|
{
|
|
get { return _selectedCells; }
|
|
}
|
|
|
|
internal EGC.SelectedCellsCollection SelectedCellsInternal
|
|
{
|
|
get { return _selectedCells; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event that fires when the SelectedCells collection changes.
|
|
/// </summary>
|
|
public event SelectedCellsChangedEventHandler SelectedCellsChanged;
|
|
|
|
/// <summary>
|
|
/// Direct notification from the SelectedCells collection of a change.
|
|
/// </summary>
|
|
internal void OnSelectedCellsChanged(NotifyCollectionChangedAction action, EGC.VirtualizedCellInfoCollection oldItems, EGC.VirtualizedCellInfoCollection newItems)
|
|
{
|
|
EGC.DataGridSelectionMode selectionMode = SelectionMode;
|
|
EGC.DataGridSelectionUnit selectionUnit = SelectionUnit;
|
|
if (!IsUpdatingSelectedCells && (selectionUnit == EGC.DataGridSelectionUnit.FullRow))
|
|
{
|
|
throw new InvalidOperationException(SR.Get(SRID.DataGrid_CannotSelectCell));
|
|
}
|
|
|
|
// Update the pending list of changes
|
|
if (oldItems != null)
|
|
{
|
|
// When IsUpdatingSelectedCells is true, there may have been cells
|
|
// added to _pendingSelectedCells that are now being removed.
|
|
// These cells should be removed from _pendingSelectedCells and
|
|
// not added to _pendingUnselectedCells.
|
|
if (_pendingSelectedCells != null)
|
|
{
|
|
EGC.VirtualizedCellInfoCollection.Xor(_pendingSelectedCells, oldItems);
|
|
}
|
|
|
|
if (_pendingUnselectedCells == null)
|
|
{
|
|
_pendingUnselectedCells = oldItems;
|
|
}
|
|
else
|
|
{
|
|
_pendingUnselectedCells.Union(oldItems);
|
|
}
|
|
}
|
|
|
|
if (newItems != null)
|
|
{
|
|
// When IsUpdatingSelectedCells is true, there may have been cells
|
|
// added to _pendingUnselectedCells that are now being removed.
|
|
// These cells should be removed from _pendingUnselectedCells and
|
|
// not added to _pendingSelectedCells.
|
|
if (_pendingUnselectedCells != null)
|
|
{
|
|
EGC.VirtualizedCellInfoCollection.Xor(_pendingUnselectedCells, newItems);
|
|
}
|
|
|
|
if (_pendingSelectedCells == null)
|
|
{
|
|
_pendingSelectedCells = newItems;
|
|
}
|
|
else
|
|
{
|
|
_pendingSelectedCells.Union(newItems);
|
|
}
|
|
}
|
|
|
|
// Not deferring change notifications
|
|
if (!IsUpdatingSelectedCells)
|
|
{
|
|
// This is most likely the case when SelectedCells was updated by
|
|
// the application. In this case, some fix-up is required, and
|
|
// the public event needs to fire.
|
|
|
|
// This will fire the event on dispose
|
|
using (UpdateSelectedCells())
|
|
{
|
|
if ((selectionMode == EGC.DataGridSelectionMode.Single) && // Single select mode
|
|
(action == NotifyCollectionChangedAction.Add) && // An item was added
|
|
(_selectedCells.Count > 1)) // There is more than one selected cell
|
|
{
|
|
// When in single selection mode and there is more than one selected
|
|
// cell, remove all cells but the new cell.
|
|
_selectedCells.RemoveAllButOne(newItems[0]);
|
|
}
|
|
else if ((action == NotifyCollectionChangedAction.Remove) &&
|
|
(oldItems != null) &&
|
|
(selectionUnit == EGC.DataGridSelectionUnit.CellOrRowHeader))
|
|
{
|
|
// If removed cells belong to rows that are selected, then the row
|
|
// needs to be unselected (other selected cells may remain selected).
|
|
bool alreadyUpdating = IsUpdatingSelectedItems;
|
|
if (!alreadyUpdating)
|
|
{
|
|
BeginUpdateSelectedItems();
|
|
}
|
|
|
|
try
|
|
{
|
|
object lastRowItem = null;
|
|
foreach (EGC.DataGridCellInfo cellInfo in oldItems)
|
|
{
|
|
// First ensure that we haven't already checked the item
|
|
object rowItem = cellInfo.Item;
|
|
if (rowItem != lastRowItem)
|
|
{
|
|
lastRowItem = rowItem;
|
|
|
|
if (SelectedItems.Contains(rowItem))
|
|
{
|
|
// Remove the item
|
|
SelectedItems.Remove(rowItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!alreadyUpdating)
|
|
{
|
|
EndUpdateSelectedItems();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fires the public change event when there are pending cell changes.
|
|
/// </summary>
|
|
private void NotifySelectedCellsChanged()
|
|
{
|
|
if (((_pendingSelectedCells != null) && (_pendingSelectedCells.Count > 0)) ||
|
|
((_pendingUnselectedCells != null) && (_pendingUnselectedCells.Count > 0)))
|
|
{
|
|
// Create the new event args
|
|
EGC.SelectedCellsChangedEventArgs e = new EGC.SelectedCellsChangedEventArgs(this, _pendingSelectedCells, _pendingUnselectedCells);
|
|
|
|
// Calculate the previous and current selection counts to determine if commands need invalidating
|
|
int currentSelectionCount = _selectedCells.Count;
|
|
int unselectedCellCount = (_pendingUnselectedCells != null) ? _pendingUnselectedCells.Count : 0;
|
|
int selectedCellCount = (_pendingSelectedCells != null) ? _pendingSelectedCells.Count : 0;
|
|
int previousSelectionCount = currentSelectionCount - selectedCellCount + unselectedCellCount;
|
|
|
|
// Clear the pending lists
|
|
_pendingSelectedCells = null;
|
|
_pendingUnselectedCells = null;
|
|
|
|
// Fire the public event
|
|
OnSelectedCellsChanged(e);
|
|
|
|
// If old or new selection is empty - invalidate Copy command
|
|
if ((previousSelectionCount == 0) || (currentSelectionCount == 0))
|
|
{
|
|
// The Copy command needs to have CanExecute run
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when there are changes to the SelectedCells collection.
|
|
/// </summary>
|
|
/// <param name="e">Event arguments that indicate which cells were added or removed.</param>
|
|
/// <remarks>
|
|
/// Base implementation fires the public SelectedCellsChanged event.
|
|
/// </remarks>
|
|
protected virtual void OnSelectedCellsChanged(EGC.SelectedCellsChangedEventArgs e)
|
|
{
|
|
if (SelectedCellsChanged != null)
|
|
{
|
|
SelectedCellsChanged(this, e);
|
|
}
|
|
|
|
// Raise automation events
|
|
if (AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementSelected) ||
|
|
AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementAddedToSelection) ||
|
|
AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection))
|
|
{
|
|
EGC.DataGridAutomationPeer peer = EGC.DataGridAutomationPeer.FromElement(this) as EGC.DataGridAutomationPeer;
|
|
if (peer != null)
|
|
{
|
|
peer.RaiseAutomationCellSelectedEvent(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A command that, when invoked, will select all items in the DataGrid.
|
|
/// </summary>
|
|
public static readonly RoutedCommand SelectAllCommand = new RoutedCommand(SR.Get(SRID.DataGrid_SelectAllCommandText), typeof(EGC.DataGrid));
|
|
|
|
private static void OnCanExecuteSelectAll(object sender, CanExecuteRoutedEventArgs e)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)sender;
|
|
e.CanExecute = (dataGrid.SelectionMode == EGC.DataGridSelectionMode.Extended) && dataGrid.IsEnabled;
|
|
e.Handled = true;
|
|
}
|
|
|
|
private static void OnExecutedSelectAll(object sender, ExecutedRoutedEventArgs e)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)sender;
|
|
if (dataGrid.SelectionUnit == EGC.DataGridSelectionUnit.Cell)
|
|
{
|
|
dataGrid.SelectAllCells();
|
|
}
|
|
else
|
|
{
|
|
dataGrid.SelectAllRows();
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void SelectAllRows()
|
|
{
|
|
int numItems = Items.Count;
|
|
int numColumns = _columns.Count;
|
|
if ((numColumns > 0) && (numItems > 0))
|
|
{
|
|
using (UpdateSelectedCells())
|
|
{
|
|
// Selecting the cells first is an optimization, which doesn't happen in a direct call to SelectAll.
|
|
_selectedCells.AddRegion(0, 0, numItems, numColumns);
|
|
SelectAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void SelectOnlyThisCell(EGC.DataGridCellInfo currentCellInfo)
|
|
{
|
|
using (UpdateSelectedCells())
|
|
{
|
|
_selectedCells.Clear();
|
|
_selectedCells.Add(currentCellInfo);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects all cells.
|
|
/// </summary>
|
|
public void SelectAllCells()
|
|
{
|
|
if (SelectionUnit == EGC.DataGridSelectionUnit.FullRow)
|
|
{
|
|
SelectAllRows();
|
|
}
|
|
else
|
|
{
|
|
int numItems = Items.Count;
|
|
int numColumns = _columns.Count;
|
|
|
|
if ((numItems > 0) && (numColumns > 0))
|
|
{
|
|
using (UpdateSelectedCells())
|
|
{
|
|
if (_selectedCells.Count > 0)
|
|
{
|
|
_selectedCells.Clear();
|
|
}
|
|
|
|
_selectedCells.AddRegion(0, 0, numItems, numColumns);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unselects all cells.
|
|
/// </summary>
|
|
public void UnselectAllCells()
|
|
{
|
|
EGC.DataGridSelectionUnit selectionUnit = SelectionUnit;
|
|
|
|
using (UpdateSelectedCells())
|
|
{
|
|
if (selectionUnit != EGC.DataGridSelectionUnit.FullRow)
|
|
{
|
|
// Unselect all of the cells
|
|
_selectedCells.Clear();
|
|
}
|
|
|
|
if (selectionUnit != EGC.DataGridSelectionUnit.Cell)
|
|
{
|
|
// Unselect all the items
|
|
UnselectAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the selection behavior.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The SelectionMode and the SelectionUnit properties together define
|
|
/// the selection behavior for the DataGrid.
|
|
/// </remarks>
|
|
public EGC.DataGridSelectionMode SelectionMode
|
|
{
|
|
get { return (EGC.DataGridSelectionMode)GetValue(SelectionModeProperty); }
|
|
set { SetValue(SelectionModeProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for the SelectionMode property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty SelectionModeProperty =
|
|
DependencyProperty.Register("SelectionMode", typeof(EGC.DataGridSelectionMode), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridSelectionMode.Extended, new PropertyChangedCallback(OnSelectionModeChanged)));
|
|
|
|
private static void OnSelectionModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
EGC.DataGridSelectionMode newSelectionMode = (EGC.DataGridSelectionMode)e.NewValue;
|
|
bool changingToSingleMode = newSelectionMode == EGC.DataGridSelectionMode.Single;
|
|
EGC.DataGridSelectionUnit selectionUnit = dataGrid.SelectionUnit;
|
|
|
|
if (changingToSingleMode && (selectionUnit == EGC.DataGridSelectionUnit.Cell))
|
|
{
|
|
// Setting CanSelectMultipleItems affects SelectedItems, but DataGrid
|
|
// needs to modify SelectedCells manually.
|
|
using (dataGrid.UpdateSelectedCells())
|
|
{
|
|
dataGrid._selectedCells.RemoveAllButOne();
|
|
}
|
|
}
|
|
|
|
// Update whether multiple items can be selected. Setting this property
|
|
// will remove items when going from multiple to single mode.
|
|
dataGrid.CanSelectMultipleItems = (newSelectionMode != EGC.DataGridSelectionMode.Single);
|
|
|
|
if (changingToSingleMode && (selectionUnit == EGC.DataGridSelectionUnit.CellOrRowHeader))
|
|
{
|
|
// In CellOrRowHeader, wait until after CanSelectMultipleItems is done removing items.
|
|
if (dataGrid.SelectedItems.Count > 0)
|
|
{
|
|
// If there is a selected item, then de-select all cells except for that one row.
|
|
using (dataGrid.UpdateSelectedCells())
|
|
{
|
|
dataGrid._selectedCells.RemoveAllButOneRow(dataGrid.Items.IndexOf(dataGrid.SelectedItems[0]));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If there is no selected item, then de-select all cells except for one.
|
|
using (dataGrid.UpdateSelectedCells())
|
|
{
|
|
dataGrid._selectedCells.RemoveAllButOne();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the selection behavior.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The SelectionMode and the SelectionUnit properties together define
|
|
/// the selection behavior for the DataGrid.
|
|
/// </remarks>
|
|
public EGC.DataGridSelectionUnit SelectionUnit
|
|
{
|
|
get { return (EGC.DataGridSelectionUnit)GetValue(SelectionUnitProperty); }
|
|
set { SetValue(SelectionUnitProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty for the SelectionUnit property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty SelectionUnitProperty =
|
|
DependencyProperty.Register("SelectionUnit", typeof(EGC.DataGridSelectionUnit), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridSelectionUnit.FullRow, new PropertyChangedCallback(OnSelectionUnitChanged)));
|
|
|
|
private static void OnSelectionUnitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
EGC.DataGridSelectionUnit oldUnit = (EGC.DataGridSelectionUnit)e.OldValue;
|
|
|
|
// Full wipe on unit change
|
|
if (oldUnit != EGC.DataGridSelectionUnit.Cell)
|
|
{
|
|
dataGrid.UnselectAll();
|
|
}
|
|
|
|
if (oldUnit != EGC.DataGridSelectionUnit.FullRow)
|
|
{
|
|
using (dataGrid.UpdateSelectedCells())
|
|
{
|
|
dataGrid._selectedCells.Clear();
|
|
}
|
|
}
|
|
|
|
dataGrid.CoerceValue(IsSynchronizedWithCurrentItemProperty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when SelectedItems changes.
|
|
/// </summary>
|
|
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
|
|
{
|
|
if (!IsUpdatingSelectedCells)
|
|
{
|
|
using (UpdateSelectedCells())
|
|
{
|
|
// Remove cells of rows that were deselected
|
|
int count = e.RemovedItems.Count;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
object rowItem = e.RemovedItems[i];
|
|
UpdateSelectionOfCellsInRow(rowItem, /* isSelected = */ false);
|
|
}
|
|
|
|
// Add cells of rows that were selected
|
|
count = e.AddedItems.Count;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
object rowItem = e.AddedItems[i];
|
|
UpdateSelectionOfCellsInRow(rowItem, /* isSelected = */ true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete depends on the selection state
|
|
CommandManager.InvalidateRequerySuggested();
|
|
|
|
// Raise automation events
|
|
if (AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementSelected) ||
|
|
AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementAddedToSelection) ||
|
|
AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection))
|
|
{
|
|
EGC.DataGridAutomationPeer peer = EGC.DataGridAutomationPeer.FromElement(this) as EGC.DataGridAutomationPeer;
|
|
if (peer != null)
|
|
{
|
|
peer.RaiseAutomationSelectionEvents(e);
|
|
}
|
|
}
|
|
|
|
base.OnSelectionChanged(e);
|
|
}
|
|
|
|
private void UpdateIsSelected()
|
|
{
|
|
UpdateIsSelected(_pendingUnselectedCells, /* isSelected = */ false);
|
|
UpdateIsSelected(_pendingSelectedCells, /* isSelected = */ true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the IsSelected property on cells due to a change in SelectedCells.
|
|
/// </summary>
|
|
private void UpdateIsSelected(EGC.VirtualizedCellInfoCollection cells, bool isSelected)
|
|
{
|
|
if (cells != null)
|
|
{
|
|
int numCells = cells.Count;
|
|
if (numCells > 0)
|
|
{
|
|
// Determine if it would be better to iterate through all the visible cells
|
|
// instead of through the update list.
|
|
bool useTracker = false;
|
|
|
|
// For "small" updates it's simpler to just go through the cells, get the container,
|
|
// and update IsSelected. For "large" updates, it's faster to go through the visible
|
|
// cells, see if they're in the collection, and then update IsSelected.
|
|
// Determining small vs. large is going to be done using a magic number.
|
|
// 750 is close to the number of visible cells Excel shows by default on a 1280x1024 monitor.
|
|
if (numCells > 750)
|
|
{
|
|
int numTracker = 0;
|
|
int numColumns = _columns.Count;
|
|
|
|
EGC.ContainerTracking<EGC.DataGridRow> rowTracker = _rowTrackingRoot;
|
|
while (rowTracker != null)
|
|
{
|
|
numTracker += numColumns;
|
|
if (numTracker >= numCells)
|
|
{
|
|
// There are more cells visible than being updated
|
|
break;
|
|
}
|
|
|
|
rowTracker = rowTracker.Next;
|
|
}
|
|
|
|
useTracker = (numCells > numTracker);
|
|
}
|
|
|
|
if (useTracker)
|
|
{
|
|
EGC.ContainerTracking<EGC.DataGridRow> rowTracker = _rowTrackingRoot;
|
|
while (rowTracker != null)
|
|
{
|
|
EGC.DataGridRow row = rowTracker.Container;
|
|
EGC.DataGridCellsPresenter cellsPresenter = row.CellsPresenter;
|
|
if (cellsPresenter != null)
|
|
{
|
|
EGC.ContainerTracking<EGC.DataGridCell> cellTracker = cellsPresenter.CellTrackingRoot;
|
|
while (cellTracker != null)
|
|
{
|
|
EGC.DataGridCell cell = cellTracker.Container;
|
|
EGC.DataGridCellInfo cellInfo = new EGC.DataGridCellInfo(cell);
|
|
if (cells.Contains(cellInfo))
|
|
{
|
|
cell.SyncIsSelected(isSelected);
|
|
}
|
|
|
|
cellTracker = cellTracker.Next;
|
|
}
|
|
}
|
|
|
|
rowTracker = rowTracker.Next;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (EGC.DataGridCellInfo cellInfo in cells)
|
|
{
|
|
EGC.DataGridCell cell = TryFindCell(cellInfo);
|
|
if (cell != null)
|
|
{
|
|
cell.SyncIsSelected(isSelected);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateSelectionOfCellsInRow(object rowItem, bool isSelected)
|
|
{
|
|
int rowIndex = Items.IndexOf(rowItem);
|
|
if (rowIndex >= 0)
|
|
{
|
|
int columnCount = _columns.Count;
|
|
if (columnCount > 0)
|
|
{
|
|
if (isSelected)
|
|
{
|
|
_selectedCells.AddRegion(rowIndex, 0, 1, columnCount);
|
|
}
|
|
else
|
|
{
|
|
_selectedCells.RemoveRegion(rowIndex, 0, 1, columnCount);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notification that a particular cell's IsSelected property changed.
|
|
/// </summary>
|
|
internal void CellIsSelectedChanged(EGC.DataGridCell cell, bool isSelected)
|
|
{
|
|
if (!IsUpdatingSelectedCells)
|
|
{
|
|
EGC.DataGridCellInfo cellInfo = new EGC.DataGridCellInfo(cell);
|
|
if (isSelected)
|
|
{
|
|
_selectedCells.AddValidatedCell(cellInfo);
|
|
}
|
|
else if (_selectedCells.Contains(cellInfo))
|
|
{
|
|
_selectedCells.Remove(cellInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// There was general input that means that selection should occur on
|
|
/// the given cell.
|
|
/// </summary>
|
|
/// <param name="cell">The target cell.</param>
|
|
/// <param name="startDragging">Whether the input also indicated that dragging should start.</param>
|
|
internal void HandleSelectionForCellInput(EGC.DataGridCell cell, bool startDragging, bool allowsExtendSelect, bool allowsMinimalSelect)
|
|
{
|
|
EGC.DataGridSelectionUnit selectionUnit = SelectionUnit;
|
|
|
|
// If the mode is None, then no selection will occur
|
|
if (selectionUnit == EGC.DataGridSelectionUnit.FullRow)
|
|
{
|
|
// In FullRow mode, items are selected
|
|
MakeFullRowSelection(cell.RowDataItem, allowsExtendSelect, allowsMinimalSelect);
|
|
}
|
|
else
|
|
{
|
|
// In the other modes, cells can be individually selected
|
|
MakeCellSelection(new EGC.DataGridCellInfo(cell), allowsExtendSelect, allowsMinimalSelect);
|
|
}
|
|
|
|
if (startDragging)
|
|
{
|
|
BeginDragging();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// There was general input on a row header that indicated that
|
|
/// selection should occur on the given row.
|
|
/// </summary>
|
|
/// <param name="row">The target row.</param>
|
|
/// <param name="startDragging">Whether the input also indicated that dragging should start.</param>
|
|
internal void HandleSelectionForRowHeaderAndDetailsInput(EGC.DataGridRow row, bool startDragging)
|
|
{
|
|
object rowItem = row.Item;
|
|
|
|
// When not dragging, move focus to the first cell
|
|
if (!_isDraggingSelection && (_columns.Count > 0))
|
|
{
|
|
if (!IsKeyboardFocusWithin)
|
|
{
|
|
// In order for CurrentCell to move focus, the
|
|
// DataGrid needs to be focused.
|
|
Focus();
|
|
}
|
|
|
|
CurrentCell = new EGC.DataGridCellInfo(rowItem, ColumnFromDisplayIndex(0), this);
|
|
}
|
|
|
|
// Select a row when the mode is not None and the unit allows selecting rows
|
|
if (CanSelectRows)
|
|
{
|
|
MakeFullRowSelection(rowItem, /* allowsExtendSelect = */ true, /* allowsMinimalSelect = */ true);
|
|
|
|
if (startDragging)
|
|
{
|
|
BeginRowDragging();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void BeginRowDragging()
|
|
{
|
|
BeginDragging();
|
|
_isRowDragging = true;
|
|
}
|
|
|
|
private void BeginDragging()
|
|
{
|
|
if (Mouse.Capture(this, CaptureMode.SubTree))
|
|
{
|
|
_isDraggingSelection = true;
|
|
_dragPoint = Mouse.GetPosition(this);
|
|
}
|
|
}
|
|
|
|
private void EndDragging()
|
|
{
|
|
StopAutoScroll();
|
|
if (Mouse.Captured == this)
|
|
{
|
|
ReleaseMouseCapture();
|
|
}
|
|
|
|
_isDraggingSelection = false;
|
|
_isRowDragging = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes selection for a row.
|
|
/// Depending on the current keyboard state, this may mean
|
|
/// - Selecting the row
|
|
/// - Deselecting the row
|
|
/// - Deselecting other rows
|
|
/// - Extending selection to the row
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ADO.Net has a bug (#524977) where if the row is in edit mode
|
|
/// and atleast one of the cells are edited and committed without
|
|
/// commiting the row itself, DataView.IndexOf for that row returns -1
|
|
/// and DataView.Contains returns false. The Workaround to this problem
|
|
/// is to try to use the previously computed row index if the operations
|
|
/// are in the same row scope.
|
|
/// </remarks>
|
|
private void MakeFullRowSelection(object dataItem, bool allowsExtendSelect, bool allowsMinimalSelect)
|
|
{
|
|
bool extendSelection = allowsExtendSelect && ShouldExtendSelection;
|
|
|
|
// minimalModify means that previous selections should not be cleared
|
|
// or that the particular item should be toggled.
|
|
bool minimalModify = allowsMinimalSelect && ShouldMinimallyModifySelection;
|
|
|
|
using (UpdateSelectedCells())
|
|
{
|
|
bool alreadyUpdating = IsUpdatingSelectedItems;
|
|
if (!alreadyUpdating)
|
|
{
|
|
BeginUpdateSelectedItems();
|
|
}
|
|
|
|
try
|
|
{
|
|
if (extendSelection)
|
|
{
|
|
// Extend selection from the anchor to the item
|
|
int numColumns = _columns.Count;
|
|
if (numColumns > 0)
|
|
{
|
|
ItemCollection items = Items;
|
|
int startIndex = items.IndexOf(_selectionAnchor.Value.Item);
|
|
int endIndex = items.IndexOf(dataItem);
|
|
if (startIndex > endIndex)
|
|
{
|
|
// Ensure that startIndex is before endIndex
|
|
int temp = startIndex;
|
|
startIndex = endIndex;
|
|
endIndex = temp;
|
|
}
|
|
|
|
if ((startIndex >= 0) && (endIndex >= 0))
|
|
{
|
|
IList selectedItems = SelectedItems;
|
|
int numItemsSelected = selectedItems.Count;
|
|
|
|
if (!minimalModify)
|
|
{
|
|
bool clearedCells = false;
|
|
|
|
// Unselect items not within the selection range
|
|
for (int index = 0; index < numItemsSelected; index++)
|
|
{
|
|
object item = selectedItems[index];
|
|
int itemIndex = items.IndexOf(item);
|
|
|
|
if ((itemIndex < startIndex) || (endIndex < itemIndex))
|
|
{
|
|
// Selector has been signaled to delay updating the
|
|
// collection until we have finished the entire update.
|
|
// The item will actually remain in the collection
|
|
// until EndUpdateSelectedItems.
|
|
selectedItems.RemoveAt(index);
|
|
|
|
if (!clearedCells)
|
|
{
|
|
// We only want to clear if something is actually being removed.
|
|
_selectedCells.Clear();
|
|
clearedCells = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we hold Control key - unselect only the previous drag selection (between CurrentCell and endIndex)
|
|
int currentCellIndex = items.IndexOf(CurrentCell.Item);
|
|
int removeRangeStartIndex = -1;
|
|
int removeRangeEndIndex = -1;
|
|
if (currentCellIndex < startIndex)
|
|
{
|
|
removeRangeStartIndex = currentCellIndex;
|
|
removeRangeEndIndex = startIndex - 1;
|
|
}
|
|
else if (currentCellIndex > endIndex)
|
|
{
|
|
removeRangeStartIndex = endIndex + 1;
|
|
removeRangeEndIndex = currentCellIndex;
|
|
}
|
|
|
|
if (removeRangeStartIndex >= 0 && removeRangeEndIndex >= 0)
|
|
{
|
|
for (int index = 0; index < numItemsSelected; index++)
|
|
{
|
|
object item = selectedItems[index];
|
|
int itemIndex = items.IndexOf(item);
|
|
|
|
if ((removeRangeStartIndex <= itemIndex) && (itemIndex <= removeRangeEndIndex))
|
|
{
|
|
// Selector has been signaled to delay updating the
|
|
// collection until we have finished the entire update.
|
|
// The item will actually remain in the collection
|
|
// until EndUpdateSelectedItems.
|
|
selectedItems.RemoveAt(index);
|
|
}
|
|
}
|
|
|
|
_selectedCells.RemoveRegion(removeRangeStartIndex, 0, removeRangeEndIndex - removeRangeStartIndex + 1, Columns.Count);
|
|
}
|
|
}
|
|
|
|
// Select the children in the selection range
|
|
IEnumerator enumerator = ((IEnumerable)items).GetEnumerator();
|
|
for (int index = 0; index <= endIndex; index++)
|
|
{
|
|
if (!enumerator.MoveNext())
|
|
{
|
|
// In case the enumerator ends unexpectedly
|
|
break;
|
|
}
|
|
|
|
if (index >= startIndex)
|
|
{
|
|
selectedItems.Add(enumerator.Current);
|
|
}
|
|
}
|
|
|
|
_selectedCells.AddRegion(startIndex, 0, endIndex - startIndex + 1, _columns.Count);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (minimalModify && SelectedItems.Contains(dataItem))
|
|
{
|
|
// Unselect the one item
|
|
UnselectItem(dataItem);
|
|
}
|
|
else
|
|
{
|
|
if (!minimalModify || !CanSelectMultipleItems)
|
|
{
|
|
// Unselect the other items
|
|
if (_selectedCells.Count > 0)
|
|
{
|
|
// Pre-emptively clear the SelectedCells collection, which is O(1),
|
|
// instead of waiting for the selection change notification to clear
|
|
// SelectedCells row by row, which is O(n).
|
|
_selectedCells.Clear();
|
|
}
|
|
|
|
if (SelectedItems.Count > 0)
|
|
{
|
|
SelectedItems.Clear();
|
|
}
|
|
}
|
|
|
|
if (_editingRowIndex >= 0 && _editingRowItem == dataItem)
|
|
{
|
|
// ADO.Net bug HACK, see remarks.
|
|
int numColumns = _columns.Count;
|
|
if (numColumns > 0)
|
|
{
|
|
_selectedCells.AddRegion(_editingRowIndex, 0, 1, numColumns);
|
|
}
|
|
|
|
SelectItem(dataItem, false);
|
|
}
|
|
else
|
|
{
|
|
// Select the item
|
|
SelectItem(dataItem);
|
|
}
|
|
}
|
|
|
|
_selectionAnchor = new EGC.DataGridCellInfo(dataItem, ColumnFromDisplayIndex(0), this);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!alreadyUpdating)
|
|
{
|
|
EndUpdateSelectedItems();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process selection on a cell.
|
|
/// Depending on the current keyboard state, this may mean
|
|
/// - Selecting the cell
|
|
/// - Deselecting the cell
|
|
/// - Deselecting other cells
|
|
/// - Extending selection to the cell
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ADO.Net has a bug (#524977) where if the row is in edit mode
|
|
/// and atleast one of the cells are edited and committed without
|
|
/// commiting the row itself, DataView.IndexOf for that row returns -1
|
|
/// and DataView.Contains returns false. The Workaround to this problem
|
|
/// is to try to use the previously computed row index if the operations
|
|
/// are in the same row scope.
|
|
/// </remarks>
|
|
private void MakeCellSelection(EGC.DataGridCellInfo cellInfo, bool allowsExtendSelect, bool allowsMinimalSelect)
|
|
{
|
|
bool extendSelection = allowsExtendSelect && ShouldExtendSelection;
|
|
|
|
// minimalModify means that previous selections should not be cleared
|
|
// or that the particular item should be toggled.
|
|
bool minimalModify = allowsMinimalSelect && ShouldMinimallyModifySelection;
|
|
|
|
using (UpdateSelectedCells())
|
|
{
|
|
int cellInfoColumnIndex = cellInfo.Column.DisplayIndex;
|
|
if (extendSelection)
|
|
{
|
|
// Extend selection from the anchor to the cell
|
|
ItemCollection items = Items;
|
|
|
|
int startIndex = items.IndexOf(_selectionAnchor.Value.Item);
|
|
int endIndex = items.IndexOf(cellInfo.Item);
|
|
|
|
if (_editingRowIndex >= 0)
|
|
{
|
|
// ADO.Net bug HACK, see remarks.
|
|
if (_selectionAnchor.Value.Item == _editingRowItem)
|
|
{
|
|
startIndex = _editingRowIndex;
|
|
}
|
|
|
|
if (cellInfo.Item == _editingRowItem)
|
|
{
|
|
endIndex = _editingRowIndex;
|
|
}
|
|
}
|
|
|
|
EGC.DataGridColumn anchorColumn = _selectionAnchor.Value.Column;
|
|
int startColumnIndex = anchorColumn.DisplayIndex;
|
|
int endColumnIndex = cellInfoColumnIndex;
|
|
|
|
if ((startIndex >= 0) && (endIndex >= 0) &&
|
|
(startColumnIndex >= 0) && (endColumnIndex >= 0))
|
|
{
|
|
int newRowCount = Math.Abs(endIndex - startIndex) + 1;
|
|
int newColumnCount = Math.Abs(endColumnIndex - startColumnIndex) + 1;
|
|
|
|
if (!minimalModify)
|
|
{
|
|
// When extending cell selection, clear out any selected items
|
|
if (SelectedItems.Count > 0)
|
|
{
|
|
UnselectAll();
|
|
}
|
|
|
|
_selectedCells.Clear();
|
|
}
|
|
else
|
|
{
|
|
// Remove the previously selected region
|
|
int currentCellIndex = items.IndexOf(CurrentCell.Item);
|
|
if (_editingRowIndex >= 0 &&
|
|
_editingRowItem == CurrentCell.Item)
|
|
{
|
|
// ADO.Net bug HACK, see remarks.
|
|
currentCellIndex = _editingRowIndex;
|
|
}
|
|
|
|
int currentCellColumnIndex = CurrentCell.Column.DisplayIndex;
|
|
|
|
int previousStartIndex = Math.Min(startIndex, currentCellIndex);
|
|
int previousRowCount = Math.Abs(currentCellIndex - startIndex) + 1;
|
|
int previousStartColumnIndex = Math.Min(startColumnIndex, currentCellColumnIndex);
|
|
int previousColumnCount = Math.Abs(currentCellColumnIndex - startColumnIndex) + 1;
|
|
|
|
_selectedCells.RemoveRegion(previousStartIndex, previousStartColumnIndex, previousRowCount, previousColumnCount);
|
|
|
|
if (SelectionUnit == EGC.DataGridSelectionUnit.CellOrRowHeader)
|
|
{
|
|
int removeRowStartIndex = previousStartIndex;
|
|
int removeRowEndIndex = previousStartIndex + previousRowCount - 1;
|
|
|
|
if (previousColumnCount <= newColumnCount)
|
|
{
|
|
// When no columns were removed, we can check fewer rows
|
|
if (previousRowCount > newRowCount)
|
|
{
|
|
// One or more rows were removed, so only check those rows
|
|
int removeCount = previousRowCount - newRowCount;
|
|
removeRowStartIndex = (previousStartIndex == currentCellIndex) ? currentCellIndex : currentCellIndex - removeCount + 1;
|
|
removeRowEndIndex = removeRowStartIndex + removeCount - 1;
|
|
}
|
|
else
|
|
{
|
|
// No rows were removed, so don't check anything
|
|
removeRowEndIndex = removeRowStartIndex - 1;
|
|
}
|
|
}
|
|
|
|
// For cells that were removed, check if their row is selected
|
|
for (int i = removeRowStartIndex; i <= removeRowEndIndex; i++)
|
|
{
|
|
object item = Items[i];
|
|
if (SelectedItems.Contains(item))
|
|
{
|
|
// When a cell in a row is unselected, unselect the row too
|
|
SelectedItems.Remove(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Select the cells in rows within the selection range
|
|
_selectedCells.AddRegion(Math.Min(startIndex, endIndex), Math.Min(startColumnIndex, endColumnIndex), newRowCount, newColumnCount);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool selectedCellsContainsCellInfo = _selectedCells.Contains(cellInfo);
|
|
bool singleRowOperation = (_editingRowIndex >= 0 && _editingRowItem == cellInfo.Item);
|
|
if (!selectedCellsContainsCellInfo &&
|
|
singleRowOperation)
|
|
{
|
|
// ADO.Net bug HACK, see remarks.
|
|
selectedCellsContainsCellInfo = _selectedCells.Contains(_editingRowIndex, cellInfoColumnIndex);
|
|
}
|
|
|
|
if (minimalModify && selectedCellsContainsCellInfo)
|
|
{
|
|
// Unselect the one cell
|
|
if (singleRowOperation)
|
|
{
|
|
// ADO.Net bug HACK, see remarks.
|
|
_selectedCells.RemoveRegion(_editingRowIndex, cellInfoColumnIndex, 1, 1);
|
|
}
|
|
else
|
|
{
|
|
_selectedCells.Remove(cellInfo);
|
|
}
|
|
|
|
if ((SelectionUnit == EGC.DataGridSelectionUnit.CellOrRowHeader) &&
|
|
SelectedItems.Contains(cellInfo.Item))
|
|
{
|
|
// When a cell in a row is unselected, unselect the row too
|
|
SelectedItems.Remove(cellInfo.Item);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!minimalModify || !CanSelectMultipleItems)
|
|
{
|
|
// Unselect any items
|
|
if (SelectedItems.Count > 0)
|
|
{
|
|
UnselectAll();
|
|
}
|
|
|
|
// Unselect all the other cells
|
|
_selectedCells.Clear();
|
|
}
|
|
|
|
if (singleRowOperation)
|
|
{
|
|
// ADO.Net bug HACK, see remarks.
|
|
_selectedCells.AddRegion(_editingRowIndex, cellInfoColumnIndex, 1, 1);
|
|
}
|
|
else
|
|
{
|
|
// Select the cell
|
|
_selectedCells.AddValidatedCell(cellInfo);
|
|
}
|
|
}
|
|
|
|
_selectionAnchor = cellInfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SelectItem(object item)
|
|
{
|
|
SelectItem(item, true);
|
|
}
|
|
|
|
private void SelectItem(object item, bool selectCells)
|
|
{
|
|
if (selectCells)
|
|
{
|
|
using (UpdateSelectedCells())
|
|
{
|
|
int itemIndex = Items.IndexOf(item);
|
|
int numColumns = _columns.Count;
|
|
if ((itemIndex >= 0) && (numColumns > 0))
|
|
{
|
|
_selectedCells.AddRegion(itemIndex, 0, 1, numColumns);
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdateSelectedItems(item, /* Add = */ true);
|
|
}
|
|
|
|
private void UnselectItem(object item)
|
|
{
|
|
using (UpdateSelectedCells())
|
|
{
|
|
int itemIndex = Items.IndexOf(item);
|
|
int numColumns = _columns.Count;
|
|
if ((itemIndex >= 0) && (numColumns > 0))
|
|
{
|
|
_selectedCells.RemoveRegion(itemIndex, 0, 1, numColumns);
|
|
}
|
|
}
|
|
|
|
UpdateSelectedItems(item, /*Add = */ false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds or Removes from SelectedItems when deferred selection is not handled by the caller.
|
|
/// </summary>
|
|
private void UpdateSelectedItems(object item, bool add)
|
|
{
|
|
bool updatingSelectedItems = IsUpdatingSelectedItems;
|
|
if (!updatingSelectedItems)
|
|
{
|
|
BeginUpdateSelectedItems();
|
|
}
|
|
|
|
try
|
|
{
|
|
if (add)
|
|
{
|
|
SelectedItems.Add(item);
|
|
}
|
|
else
|
|
{
|
|
SelectedItems.Remove(item);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!updatingSelectedItems)
|
|
{
|
|
EndUpdateSelectedItems();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When changing SelectedCells, do:
|
|
/// using (UpdateSelectedCells())
|
|
/// {
|
|
/// ...
|
|
/// }
|
|
/// </summary>
|
|
private IDisposable UpdateSelectedCells()
|
|
{
|
|
return new ChangingSelectedCellsHelper(this);
|
|
}
|
|
|
|
private void BeginUpdateSelectedCells()
|
|
{
|
|
Debug.Assert(!IsUpdatingSelectedCells);
|
|
_updatingSelectedCells = true;
|
|
}
|
|
|
|
private void EndUpdateSelectedCells()
|
|
{
|
|
Debug.Assert(IsUpdatingSelectedCells);
|
|
|
|
UpdateIsSelected();
|
|
_updatingSelectedCells = false;
|
|
|
|
NotifySelectedCellsChanged();
|
|
}
|
|
|
|
private bool IsUpdatingSelectedCells
|
|
{
|
|
get { return _updatingSelectedCells; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles tracking defered selection change notifications for selected cells.
|
|
/// </summary>
|
|
private class ChangingSelectedCellsHelper : IDisposable
|
|
{
|
|
internal ChangingSelectedCellsHelper(EGC.DataGrid dataGrid)
|
|
{
|
|
_dataGrid = dataGrid;
|
|
_wasUpdatingSelectedCells = _dataGrid.IsUpdatingSelectedCells;
|
|
if (!_wasUpdatingSelectedCells)
|
|
{
|
|
_dataGrid.BeginUpdateSelectedCells();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_wasUpdatingSelectedCells)
|
|
{
|
|
_dataGrid.EndUpdateSelectedCells();
|
|
}
|
|
}
|
|
|
|
private EGC.DataGrid _dataGrid;
|
|
private bool _wasUpdatingSelectedCells;
|
|
}
|
|
|
|
/// <summary>
|
|
/// SHIFT is down or performing a drag selection.
|
|
/// Multiple items can be selected.
|
|
/// There is a selection anchor.
|
|
/// </summary>
|
|
private bool ShouldExtendSelection
|
|
{
|
|
get
|
|
{
|
|
return CanSelectMultipleItems && (_selectionAnchor != null) &&
|
|
(_isDraggingSelection || ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// CTRL is down.
|
|
/// Previous selection should not be cleared, or a selected item should be toggled.
|
|
/// </summary>
|
|
private static bool ShouldMinimallyModifySelection
|
|
{
|
|
get
|
|
{
|
|
return (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
|
|
}
|
|
}
|
|
|
|
private bool CanSelectRows
|
|
{
|
|
get
|
|
{
|
|
switch (SelectionUnit)
|
|
{
|
|
case EGC.DataGridSelectionUnit.FullRow:
|
|
case EGC.DataGridSelectionUnit.CellOrRowHeader:
|
|
return true;
|
|
|
|
case EGC.DataGridSelectionUnit.Cell:
|
|
return false;
|
|
}
|
|
|
|
Debug.Fail("Unknown SelectionUnit encountered.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
_currentCellContainer = null;
|
|
|
|
using (UpdateSelectedCells())
|
|
{
|
|
// Send the change notification to the selected cells collection
|
|
_selectedCells.OnItemsCollectionChanged(e, SelectedItems);
|
|
}
|
|
|
|
if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace)
|
|
{
|
|
foreach (object item in e.OldItems)
|
|
{
|
|
_itemAttachedStorage.ClearItem(item);
|
|
}
|
|
}
|
|
else if (e.Action == NotifyCollectionChangedAction.Reset)
|
|
{
|
|
_itemAttachedStorage.Clear();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Input
|
|
|
|
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
d.CoerceValue(CanUserAddRowsProperty);
|
|
d.CoerceValue(CanUserDeleteRowsProperty);
|
|
|
|
// Many commands use IsEnabled to determine if they are enabled or not
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when a keyboard key is pressed.
|
|
/// </summary>
|
|
protected override void OnKeyDown(KeyEventArgs e)
|
|
{
|
|
switch (e.Key)
|
|
{
|
|
case Key.Tab:
|
|
OnTabKeyDown(e);
|
|
break;
|
|
|
|
case Key.Enter:
|
|
OnEnterKeyDown(e);
|
|
break;
|
|
|
|
case Key.Left:
|
|
case Key.Right:
|
|
case Key.Up:
|
|
case Key.Down:
|
|
OnArrowKeyDown(e);
|
|
break;
|
|
|
|
case Key.Home:
|
|
case Key.End:
|
|
OnHomeOrEndKeyDown(e);
|
|
break;
|
|
|
|
case Key.PageUp:
|
|
case Key.PageDown:
|
|
OnPageUpOrDownKeyDown(e);
|
|
break;
|
|
}
|
|
|
|
if (!e.Handled)
|
|
{
|
|
base.OnKeyDown(e);
|
|
}
|
|
}
|
|
|
|
private static FocusNavigationDirection KeyToTraversalDirection(Key key)
|
|
{
|
|
switch (key)
|
|
{
|
|
case Key.Left:
|
|
return FocusNavigationDirection.Left;
|
|
|
|
case Key.Right:
|
|
return FocusNavigationDirection.Right;
|
|
|
|
case Key.Up:
|
|
return FocusNavigationDirection.Up;
|
|
|
|
case Key.Down:
|
|
default:
|
|
return FocusNavigationDirection.Down;
|
|
}
|
|
}
|
|
|
|
private void OnArrowKeyDown(KeyEventArgs e)
|
|
{
|
|
EGC.DataGridCell currentCellContainer = CurrentCellContainer;
|
|
if (currentCellContainer != null)
|
|
{
|
|
e.Handled = true;
|
|
bool wasEditing = currentCellContainer.IsEditing;
|
|
|
|
UIElement startElement = Keyboard.FocusedElement as UIElement;
|
|
ContentElement startContentElement = (startElement == null) ? Keyboard.FocusedElement as ContentElement : null;
|
|
if ((startElement != null) || (startContentElement != null))
|
|
{
|
|
bool navigateFromCellContainer = e.OriginalSource == currentCellContainer;
|
|
if (navigateFromCellContainer)
|
|
{
|
|
KeyboardNavigationMode keyboardNavigationMode = KeyboardNavigation.GetDirectionalNavigation(this);
|
|
if (keyboardNavigationMode == KeyboardNavigationMode.Once)
|
|
{
|
|
// KeyboardNavigation will move the focus out of the DataGrid
|
|
DependencyObject nextFocusTarget = this.PredictFocus(KeyToTraversalDirection(e.Key));
|
|
if (nextFocusTarget != null && !this.IsAncestorOf(nextFocusTarget))
|
|
{
|
|
Keyboard.Focus(nextFocusTarget as IInputElement);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
int currentDisplayIndex = this.CurrentColumn.DisplayIndex;
|
|
int currentRowIndex = Items.IndexOf(CurrentItem);
|
|
int nextDisplayIndex = currentDisplayIndex;
|
|
int nextRowIndex = currentRowIndex;
|
|
bool controlModifier = ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == ModifierKeys.Control);
|
|
|
|
// Reverse the navigation in RTL flow direction
|
|
Key rtlKey = e.Key;
|
|
if (this.FlowDirection == FlowDirection.RightToLeft)
|
|
{
|
|
if (rtlKey == Key.Left)
|
|
{
|
|
rtlKey = Key.Right;
|
|
}
|
|
else if (rtlKey == Key.Right)
|
|
{
|
|
rtlKey = Key.Left;
|
|
}
|
|
}
|
|
|
|
switch (rtlKey)
|
|
{
|
|
case Key.Left:
|
|
if (controlModifier)
|
|
{
|
|
nextDisplayIndex = InternalColumns.FirstVisibleDisplayIndex;
|
|
}
|
|
else
|
|
{
|
|
nextDisplayIndex--;
|
|
while (nextDisplayIndex >= 0)
|
|
{
|
|
EGC.DataGridColumn column = ColumnFromDisplayIndex(nextDisplayIndex);
|
|
if (column.IsVisible)
|
|
{
|
|
break;
|
|
}
|
|
|
|
nextDisplayIndex--;
|
|
}
|
|
|
|
if (nextDisplayIndex < 0)
|
|
{
|
|
if (keyboardNavigationMode == KeyboardNavigationMode.Cycle)
|
|
{
|
|
nextDisplayIndex = InternalColumns.LastVisibleDisplayIndex;
|
|
}
|
|
else if (keyboardNavigationMode == KeyboardNavigationMode.Contained)
|
|
{
|
|
return;
|
|
}
|
|
else // Continue, Local, None - move focus out of the datagrid
|
|
{
|
|
MoveFocus(new TraversalRequest(e.Key == Key.Left ? FocusNavigationDirection.Left : FocusNavigationDirection.Right));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case Key.Right:
|
|
if (controlModifier)
|
|
{
|
|
nextDisplayIndex = Math.Max(0, InternalColumns.LastVisibleDisplayIndex);
|
|
}
|
|
else
|
|
{
|
|
nextDisplayIndex++;
|
|
int columnCount = Columns.Count;
|
|
while (nextDisplayIndex < columnCount)
|
|
{
|
|
EGC.DataGridColumn column = ColumnFromDisplayIndex(nextDisplayIndex);
|
|
if (column.IsVisible)
|
|
{
|
|
break;
|
|
}
|
|
|
|
nextDisplayIndex++;
|
|
}
|
|
|
|
if (nextDisplayIndex >= Columns.Count)
|
|
{
|
|
if (keyboardNavigationMode == KeyboardNavigationMode.Cycle)
|
|
{
|
|
nextDisplayIndex = InternalColumns.FirstVisibleDisplayIndex;
|
|
}
|
|
else if (keyboardNavigationMode == KeyboardNavigationMode.Contained)
|
|
{
|
|
return;
|
|
}
|
|
else // Continue, Local, None - move focus out of the datagrid
|
|
{
|
|
MoveFocus(new TraversalRequest(e.Key == Key.Left ? FocusNavigationDirection.Left : FocusNavigationDirection.Right));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case Key.Up:
|
|
if (controlModifier)
|
|
{
|
|
nextRowIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
nextRowIndex--;
|
|
if (nextRowIndex < 0)
|
|
{
|
|
if (keyboardNavigationMode == KeyboardNavigationMode.Cycle)
|
|
{
|
|
nextRowIndex = Items.Count - 1;
|
|
}
|
|
else if (keyboardNavigationMode == KeyboardNavigationMode.Contained)
|
|
{
|
|
return;
|
|
}
|
|
else // Continue, Local, None - move focus out of the datagrid
|
|
{
|
|
MoveFocus(new TraversalRequest(FocusNavigationDirection.Up));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case Key.Down:
|
|
default:
|
|
if (controlModifier)
|
|
{
|
|
nextRowIndex = Math.Max(0, Items.Count - 1);
|
|
}
|
|
else
|
|
{
|
|
nextRowIndex++;
|
|
if (nextRowIndex >= Items.Count)
|
|
{
|
|
if (keyboardNavigationMode == KeyboardNavigationMode.Cycle)
|
|
{
|
|
nextRowIndex = 0;
|
|
}
|
|
else if (keyboardNavigationMode == KeyboardNavigationMode.Contained)
|
|
{
|
|
return;
|
|
}
|
|
else // Continue, Local, None - move focus out of the datagrid
|
|
{
|
|
MoveFocus(new TraversalRequest(FocusNavigationDirection.Down));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
EGC.DataGridColumn nextColumn = ColumnFromDisplayIndex(nextDisplayIndex);
|
|
object nextItem = Items[nextRowIndex];
|
|
ScrollCellIntoView(nextItem, nextColumn);
|
|
EGC.DataGridCell nextCellContainer = TryFindCell(nextItem, nextColumn);
|
|
|
|
if (nextCellContainer == null || nextCellContainer == currentCellContainer || !nextCellContainer.Focus())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Attempt to move focus
|
|
TraversalRequest request = new TraversalRequest(KeyToTraversalDirection(e.Key));
|
|
if (navigateFromCellContainer ||
|
|
((startElement != null) && startElement.MoveFocus(request)) ||
|
|
((startContentElement != null) && startContentElement.MoveFocus(request)))
|
|
{
|
|
SelectAndEditOnFocusMove(e, currentCellContainer, wasEditing, /* allowsExtendSelect = */ true, /* ignoreControlKey = */ true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the tab key is pressed to perform focus navigation.
|
|
/// </summary>
|
|
private void OnTabKeyDown(KeyEventArgs e)
|
|
{
|
|
// When the end-user uses the keyboard to tab to another cell while the current cell
|
|
// is in edit-mode, then the next cell should enter edit mode in addition to gaining
|
|
// focus. There is no way to detect this from the focus change events, so the cell
|
|
// is going to handle the complete operation manually.
|
|
// The standard focus change method is being called here, so even if focus moves
|
|
// to something other than a cell, focus should land on the element that it would
|
|
// have landed on anyway.
|
|
EGC.DataGridCell currentCellContainer = CurrentCellContainer;
|
|
if (currentCellContainer != null)
|
|
{
|
|
bool wasEditing = currentCellContainer.IsEditing;
|
|
bool previous = ((e.KeyboardDevice.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift);
|
|
|
|
// Start navigation from the current focus to allow moveing focus on other focusable elements inside the cell
|
|
UIElement startElement = Keyboard.FocusedElement as UIElement;
|
|
ContentElement startContentElement = (startElement == null) ? Keyboard.FocusedElement as ContentElement : null;
|
|
if ((startElement != null) || (startContentElement != null))
|
|
{
|
|
e.Handled = true;
|
|
|
|
FocusNavigationDirection direction = previous ? FocusNavigationDirection.Previous : FocusNavigationDirection.Next;
|
|
TraversalRequest request = new TraversalRequest(direction);
|
|
request.Wrapped = true; // Navigate only within datagrid
|
|
|
|
// Move focus to the the next or previous tab stop.
|
|
if (((startElement != null) && startElement.MoveFocus(request)) ||
|
|
((startContentElement != null) && startContentElement.MoveFocus(request)))
|
|
{
|
|
// If focus moved to the cell while in edit mode - keep navigating to the previous cell
|
|
if (wasEditing && previous && Keyboard.FocusedElement == currentCellContainer)
|
|
{
|
|
currentCellContainer.MoveFocus(request);
|
|
}
|
|
else
|
|
{
|
|
UIElement tempElement = Keyboard.FocusedElement as UIElement;
|
|
if (tempElement != null && tempElement is EGC.DataGridCell)
|
|
{
|
|
tempElement.MoveFocus(request);
|
|
}
|
|
}
|
|
|
|
// In case of grouping if a row level commit happened due to
|
|
// the previous focus change, the container of the row gets
|
|
// removed from the visual tree by the CollectionView,
|
|
// but we still hang on to a cell of that row, which will be used
|
|
// by the call to SelectAndEditOnFocusMove. Hence re-establishing the
|
|
// focus appropriately in such cases.
|
|
if (IsGrouping && wasEditing)
|
|
{
|
|
EGC.DataGridCell newCell = GetCellForSelectAndEditOnFocusMove();
|
|
|
|
if (newCell != null &&
|
|
newCell.RowDataItem == currentCellContainer.RowDataItem)
|
|
{
|
|
EGC.DataGridCell realNewCell = TryFindCell(newCell.RowDataItem, newCell.Column);
|
|
|
|
// Forcing an UpdateLayout since the generation of the new row
|
|
// container which was removed earlier is done in measure.
|
|
if (realNewCell == null)
|
|
{
|
|
UpdateLayout();
|
|
realNewCell = TryFindCell(newCell.RowDataItem, newCell.Column);
|
|
}
|
|
if (realNewCell != null && realNewCell != newCell)
|
|
{
|
|
realNewCell.Focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
// When doing TAB and SHIFT+TAB focus movement, don't confuse the selection
|
|
// code, which also relies on SHIFT to know whether to extend selection or not.
|
|
SelectAndEditOnFocusMove(e, currentCellContainer, wasEditing, /* allowsExtendSelect = */ false, /* ignoreControlKey = */ true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnEnterKeyDown(KeyEventArgs e)
|
|
{
|
|
EGC.DataGridCell currentCellContainer = CurrentCellContainer;
|
|
if ((currentCellContainer != null) && (_columns.Count > 0))
|
|
{
|
|
e.Handled = true;
|
|
|
|
EGC.DataGridColumn column = currentCellContainer.Column;
|
|
|
|
// Commit any current edit
|
|
if (CommitAnyEdit() && ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == 0))
|
|
{
|
|
bool shiftModifier = ((e.KeyboardDevice.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift);
|
|
|
|
// Go to the next row, keeping the column the same
|
|
int numItems = Items.Count;
|
|
int index = Math.Max(0, Math.Min(numItems - 1, Items.IndexOf(currentCellContainer.RowDataItem) + (shiftModifier ? -1 : 1)));
|
|
|
|
if (index < numItems)
|
|
{
|
|
object rowItem = Items[index];
|
|
ScrollIntoView(rowItem, column);
|
|
|
|
if (CurrentCell.Item != rowItem)
|
|
{
|
|
// Focus the new cell
|
|
CurrentCell = new EGC.DataGridCellInfo(rowItem, column, this);
|
|
|
|
// Will never edit on ENTER, so just say that the old cell wasn't in edit mode
|
|
SelectAndEditOnFocusMove(e, currentCellContainer, /* wasEditing = */ false, /* allowsExtendSelect = */ false, /* ignoreControlKey = */ true);
|
|
}
|
|
else
|
|
{
|
|
// When the new item jumped to the bottom, CurrentCell doesn't actually change,
|
|
// but there is a new container.
|
|
currentCellContainer = CurrentCellContainer;
|
|
if (currentCellContainer != null)
|
|
{
|
|
currentCellContainer.Focus();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private EGC.DataGridCell GetCellForSelectAndEditOnFocusMove()
|
|
{
|
|
EGC.DataGridCell newCell = Keyboard.FocusedElement as EGC.DataGridCell;
|
|
|
|
// If focus has moved within DataGridCell use CurrentCellContainer
|
|
if (newCell == null && CurrentCellContainer != null && CurrentCellContainer.IsKeyboardFocusWithin)
|
|
{
|
|
newCell = CurrentCellContainer;
|
|
}
|
|
|
|
return newCell;
|
|
}
|
|
|
|
private void SelectAndEditOnFocusMove(KeyEventArgs e, EGC.DataGridCell oldCell, bool wasEditing, bool allowsExtendSelect, bool ignoreControlKey)
|
|
{
|
|
EGC.DataGridCell newCell = GetCellForSelectAndEditOnFocusMove();
|
|
|
|
if ((newCell != null) && (newCell.DataGridOwner == this))
|
|
{
|
|
if (ignoreControlKey || ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == 0))
|
|
{
|
|
HandleSelectionForCellInput(newCell, /* startDragging = */ false, allowsExtendSelect, /* allowsMinimalSelect = */ false);
|
|
}
|
|
|
|
// If focus moved to a new cell within the same row that didn't
|
|
// decide on its own to enter edit mode, put it in edit mode.
|
|
if (wasEditing && !newCell.IsEditing && (oldCell.RowDataItem == newCell.RowDataItem))
|
|
{
|
|
BeginEdit(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnHomeOrEndKeyDown(KeyEventArgs e)
|
|
{
|
|
if ((_columns.Count > 0) && (Items.Count > 0))
|
|
{
|
|
e.Handled = true;
|
|
|
|
bool homeKey = (e.Key == Key.Home);
|
|
bool controlModifier = ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == ModifierKeys.Control);
|
|
|
|
// Go to the first or last cell
|
|
object item = controlModifier ? Items[homeKey ? 0 : Items.Count - 1] : CurrentItem;
|
|
EGC.DataGridColumn column = ColumnFromDisplayIndex(homeKey ? InternalColumns.FirstVisibleDisplayIndex : InternalColumns.LastVisibleDisplayIndex);
|
|
|
|
ScrollCellIntoView(item, column);
|
|
EGC.DataGridCell cell = TryFindCell(item, column);
|
|
if (cell != null)
|
|
{
|
|
cell.Focus();
|
|
HandleSelectionForCellInput(cell, /* startDragging = */ false, /* allowsExtendSelect = */ true, /* allowsMinimalSelect = */ false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnPageUpOrDownKeyDown(KeyEventArgs e)
|
|
{
|
|
// This code relies on DataGridRowsPresenter since ScrollHost relies
|
|
// on InternalItemsHost, which relies on DataGridRowsPresenter.
|
|
// Additionally, it relies on ViewportHeight being in logical units
|
|
// instead of pixels.
|
|
ScrollViewer scrollHost = InternalScrollHost;
|
|
if (scrollHost != null)
|
|
{
|
|
object currentRow = CurrentItem;
|
|
EGC.DataGridColumn currentColumn = CurrentColumn;
|
|
int rowIndex = Items.IndexOf(currentRow);
|
|
if (rowIndex >= 0)
|
|
{
|
|
// Predict the page up/page down item based on the viewport height, which
|
|
// should be in logical units.
|
|
// This is not going to work well when the rows have different heights, but
|
|
// it is the best estimate we have at the moment.
|
|
int jumpDistance = Math.Max(1, (int)scrollHost.ViewportHeight - 1);
|
|
int targetIndex = (e.Key == Key.PageUp) ? rowIndex - jumpDistance : rowIndex + jumpDistance;
|
|
targetIndex = Math.Max(0, Math.Min(targetIndex, Items.Count - 1));
|
|
|
|
// Scroll the target row into view, keeping the current column
|
|
object targetRow = Items[targetIndex];
|
|
ScrollCellIntoView(targetRow, currentColumn);
|
|
EGC.DataGridCell cell = TryFindCell(targetRow, currentColumn);
|
|
if (cell != null)
|
|
{
|
|
cell.Focus();
|
|
HandleSelectionForCellInput(cell, /* startDragging = */ false, /* allowsExtendSelect = */ true, /* allowsMinimalSelect = */ false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Continues a drag selection.
|
|
/// </summary>
|
|
protected override void OnMouseMove(MouseEventArgs e)
|
|
{
|
|
if (_isDraggingSelection)
|
|
{
|
|
if (e.LeftButton == MouseButtonState.Pressed)
|
|
{
|
|
// Check that the mouse has moved relative to the DataGrid.
|
|
// This check prevents the case where a row is partially visible
|
|
// at the bottom. If this row is clicked, then it will be scrolled
|
|
// into view and away from the mouse. The mouse will then appear
|
|
// (according to these messages) as if it moved over a new cell, and
|
|
// could invoke a drag, but the actual mouse position relative to
|
|
// the DataGrid hasn't changed.
|
|
Point currentMousePosition = Mouse.GetPosition(this);
|
|
if (!DoubleUtil.AreClose(currentMousePosition, _dragPoint))
|
|
{
|
|
_dragPoint = currentMousePosition;
|
|
|
|
RelativeMousePositions position = RelativeMousePosition;
|
|
if (position == RelativeMousePositions.Over)
|
|
{
|
|
// The mouse is within the field of cells and rows, use the actual
|
|
// elements to determine changes to selection.
|
|
if (_isRowDragging)
|
|
{
|
|
EGC.DataGridRow row = MouseOverRow;
|
|
if ((row != null) && (row.Item != CurrentItem))
|
|
{
|
|
// Continue a row header drag to the given row
|
|
HandleSelectionForRowHeaderAndDetailsInput(row, /* startDragging = */ false);
|
|
CurrentItem = row.Item;
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
EGC.DataGridCell cell = MouseOverCell;
|
|
if (cell == null)
|
|
{
|
|
EGC.DataGridRow row = MouseOverRow;
|
|
if (row != null)
|
|
{
|
|
// The mouse is over a row but not necessarily a cell,
|
|
// such as over a header or details section. Find the
|
|
// nearest cell and use that.
|
|
cell = GetCellNearMouse();
|
|
}
|
|
}
|
|
|
|
if ((cell != null) && (cell != CurrentCellContainer))
|
|
{
|
|
HandleSelectionForCellInput(cell, /* startDragging = */ false, /* allowsExtendSelect = */ true, /* allowsMinimalSelect = */ true);
|
|
cell.Focus();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The mouse is outside of the field of cells and rows.
|
|
if (_isRowDragging && IsMouseToLeftOrRightOnly(position))
|
|
{
|
|
// Figure out which row the mouse is in-line with and select it
|
|
EGC.DataGridRow row = GetRowNearMouse();
|
|
if ((row != null) && (row.Item != CurrentItem))
|
|
{
|
|
// The mouse is directly to the left or right of the row
|
|
HandleSelectionForRowHeaderAndDetailsInput(row, /* startDragging = */ false);
|
|
CurrentItem = row.Item;
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
else if (_hasAutoScrolled)
|
|
{
|
|
// The mouse is outside the grid, and we've started auto-scrolling.
|
|
// The user has moved the mouse and would like a quick update.
|
|
if (DoAutoScroll())
|
|
{
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Ensure that the auto-scroll timer has started
|
|
//call NativeMethods.GetDoubleClickTime() may throw exception, so comment out this function.
|
|
//StartAutoScroll();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The mouse button is up, end the drag operation
|
|
EndDragging();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void OnAnyMouseUpThunk(object sender, MouseButtonEventArgs e)
|
|
{
|
|
((EGC.DataGrid)sender).OnAnyMouseUp(e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends a drag selection.
|
|
/// </summary>
|
|
private void OnAnyMouseUp(MouseButtonEventArgs e)
|
|
{
|
|
EndDragging();
|
|
}
|
|
|
|
/// <summary>
|
|
/// When a ContextMenu opens on a cell that isn't selected, it should
|
|
/// become selected.
|
|
/// </summary>
|
|
protected override void OnContextMenuOpening(ContextMenuEventArgs e)
|
|
{
|
|
EGC.DataGridCell cell = null;
|
|
EGC.DataGridRowHeader rowHeader = null;
|
|
UIElement sourceElement = e.OriginalSource as UIElement;
|
|
while (sourceElement != null)
|
|
{
|
|
cell = sourceElement as EGC.DataGridCell;
|
|
if (cell != null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
rowHeader = sourceElement as EGC.DataGridRowHeader;
|
|
if (rowHeader != null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
sourceElement = VisualTreeHelper.GetParent(sourceElement) as UIElement;
|
|
}
|
|
|
|
if ((cell != null) && !cell.IsSelected && !cell.IsKeyboardFocusWithin)
|
|
{
|
|
cell.Focus();
|
|
HandleSelectionForCellInput(cell, /* startDragging = */ false, /* allowsExtendSelect = */ true, /* allowsMinimalSelect = */ true);
|
|
}
|
|
|
|
if (rowHeader != null)
|
|
{
|
|
EGC.DataGridRow parentRow = rowHeader.ParentRow;
|
|
if (parentRow != null)
|
|
{
|
|
HandleSelectionForRowHeaderAndDetailsInput(parentRow, /* startDragging = */ false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the row that contains the mouse's Y coordinate.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Relies on InternalItemsHost.
|
|
/// Meant to be used when the mouse is outside the DataGrid.
|
|
/// </remarks>
|
|
private EGC.DataGridRow GetRowNearMouse()
|
|
{
|
|
Debug.Assert(RelativeMousePosition != RelativeMousePositions.Over, "The mouse is not supposed to be over the DataGrid.");
|
|
|
|
Panel itemsHost = InternalItemsHost;
|
|
if (itemsHost != null)
|
|
{
|
|
// Iterate from the end to the beginning since it is more common
|
|
// to drag toward the end.
|
|
int count = itemsHost.Children.Count;
|
|
for (int i = count - 1; i >= 0; i--)
|
|
{
|
|
EGC.DataGridRow row = itemsHost.Children[i] as EGC.DataGridRow;
|
|
if (row != null)
|
|
{
|
|
Point pt = Mouse.GetPosition(row);
|
|
Rect rowBounds = new Rect(new Point(), row.RenderSize);
|
|
if ((pt.Y >= rowBounds.Top) && (pt.Y <= rowBounds.Bottom))
|
|
{
|
|
// The mouse cursor's Y position is within the Y bounds of the row
|
|
return row;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the cell that is nearest to the mouse.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Relies on InternalItemsHost.
|
|
/// </remarks>
|
|
private EGC.DataGridCell GetCellNearMouse()
|
|
{
|
|
Panel itemsHost = InternalItemsHost;
|
|
if (itemsHost != null)
|
|
{
|
|
Rect itemsHostBounds = new Rect(new Point(), itemsHost.RenderSize);
|
|
double closestDistance = Double.PositiveInfinity;
|
|
EGC.DataGridCell closestCell = null;
|
|
bool isMouseInCorner = IsMouseInCorner(RelativeMousePosition);
|
|
|
|
// Iterate from the end to the beginning since it is more common
|
|
// to drag toward the end.
|
|
int count = itemsHost.Children.Count;
|
|
for (int i = count - 1; i >= 0; i--)
|
|
{
|
|
EGC.DataGridRow row = itemsHost.Children[i] as EGC.DataGridRow;
|
|
if (row != null)
|
|
{
|
|
EGC.DataGridCellsPresenter cellsPresenter = row.CellsPresenter;
|
|
if (cellsPresenter != null)
|
|
{
|
|
// Go through all of the instantiated cells and find the closest cell
|
|
EGC.ContainerTracking<EGC.DataGridCell> cellTracker = cellsPresenter.CellTrackingRoot;
|
|
while (cellTracker != null)
|
|
{
|
|
EGC.DataGridCell cell = cellTracker.Container;
|
|
|
|
double cellDistance;
|
|
if (CalculateCellDistance(cell, row, itemsHost, itemsHostBounds, isMouseInCorner, out cellDistance))
|
|
{
|
|
if ((closestCell == null) || (cellDistance < closestDistance))
|
|
{
|
|
// This cell's distance is less, so make it the closest cell
|
|
closestDistance = cellDistance;
|
|
closestCell = cell;
|
|
}
|
|
}
|
|
|
|
cellTracker = cellTracker.Next;
|
|
}
|
|
|
|
// Check if the header is close
|
|
EGC.DataGridRowHeader rowHeader = row.RowHeader;
|
|
if (rowHeader != null)
|
|
{
|
|
double cellDistance;
|
|
if (CalculateCellDistance(rowHeader, row, itemsHost, itemsHostBounds, isMouseInCorner, out cellDistance))
|
|
{
|
|
if ((closestCell == null) || (cellDistance < closestDistance))
|
|
{
|
|
// If the header is the closest, then use the first cell from the row
|
|
EGC.DataGridCell cell = row.TryGetCell(DisplayIndexMap[0]);
|
|
if (cell != null)
|
|
{
|
|
closestDistance = cellDistance;
|
|
closestCell = cell;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return closestCell;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a cell meets the criteria for being chosen. If it does, it
|
|
/// calculates its a "distance" that can be compared to other cells.
|
|
/// </summary>
|
|
/// <param name="distance">
|
|
/// A value that represents the distance between the mouse and the cell.
|
|
/// This is not necessarily an accurate pixel number in some cases.
|
|
/// </param>
|
|
/// <returns>
|
|
/// true if the cell can be a drag target. false otherwise.
|
|
/// </returns>
|
|
private static bool CalculateCellDistance(FrameworkElement cell, EGC.DataGridRow rowOwner, Panel itemsHost, Rect itemsHostBounds, bool isMouseInCorner, out double distance)
|
|
{
|
|
GeneralTransform transform = cell.TransformToAncestor(itemsHost);
|
|
Rect cellBounds = new Rect(new Point(), cell.RenderSize);
|
|
|
|
// Limit to only cells that are entirely visible
|
|
if (itemsHostBounds.Contains(transform.TransformBounds(cellBounds)))
|
|
{
|
|
Point pt = Mouse.GetPosition(cell);
|
|
if (isMouseInCorner)
|
|
{
|
|
// When the mouse is in the corner, go by distance from center of the cell
|
|
Vector v = new Vector(pt.X - (cellBounds.Width * 0.5), pt.Y - (cellBounds.Height * 0.5));
|
|
distance = v.Length;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Point rowPt = Mouse.GetPosition(rowOwner);
|
|
Rect rowBounds = new Rect(new Point(), rowOwner.RenderSize);
|
|
|
|
// The mouse should overlap a row or column
|
|
if ((pt.X >= cellBounds.Left) && (pt.X <= cellBounds.Right))
|
|
{
|
|
// The mouse is within a column
|
|
if ((rowPt.Y >= rowBounds.Top) && (rowPt.Y <= rowBounds.Bottom))
|
|
{
|
|
// Mouse is within the cell
|
|
distance = 0.0;
|
|
}
|
|
else
|
|
{
|
|
// Mouse is outside but is within a columns horizontal bounds
|
|
distance = Math.Abs(pt.Y - cellBounds.Top);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else if ((rowPt.Y >= rowBounds.Top) && (rowPt.Y <= rowBounds.Bottom))
|
|
{
|
|
// Mouse is outside but is within a row's vertical bounds
|
|
distance = Math.Abs(pt.X - cellBounds.Left);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
distance = Double.PositiveInfinity;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The row that the mouse is over.
|
|
/// </summary>
|
|
private static EGC.DataGridRow MouseOverRow
|
|
{
|
|
get
|
|
{
|
|
return EGC.DataGridHelper.FindVisualParent<EGC.DataGridRow>(Mouse.DirectlyOver as UIElement);
|
|
}
|
|
}
|
|
|
|
// The cell that the mouse is over.
|
|
private static EGC.DataGridCell MouseOverCell
|
|
{
|
|
get
|
|
{
|
|
return EGC.DataGridHelper.FindVisualParent<EGC.DataGridCell>(Mouse.DirectlyOver as UIElement);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The mouse position relative to the ItemsHost.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Relies on InternalItemsHost.
|
|
/// </remarks>
|
|
private RelativeMousePositions RelativeMousePosition
|
|
{
|
|
get
|
|
{
|
|
RelativeMousePositions position = RelativeMousePositions.Over;
|
|
|
|
Panel itemsHost = InternalItemsHost;
|
|
if (itemsHost != null)
|
|
{
|
|
Point pt = Mouse.GetPosition(itemsHost);
|
|
Rect bounds = new Rect(new Point(), itemsHost.RenderSize);
|
|
|
|
if (pt.X < bounds.Left)
|
|
{
|
|
position |= RelativeMousePositions.Left;
|
|
}
|
|
else if (pt.X > bounds.Right)
|
|
{
|
|
position |= RelativeMousePositions.Right;
|
|
}
|
|
|
|
if (pt.Y < bounds.Top)
|
|
{
|
|
position |= RelativeMousePositions.Above;
|
|
}
|
|
else if (pt.Y > bounds.Bottom)
|
|
{
|
|
position |= RelativeMousePositions.Below;
|
|
}
|
|
}
|
|
|
|
return position;
|
|
}
|
|
}
|
|
|
|
private static bool IsMouseToLeft(RelativeMousePositions position)
|
|
{
|
|
return (position & RelativeMousePositions.Left) == RelativeMousePositions.Left;
|
|
}
|
|
|
|
private static bool IsMouseToRight(RelativeMousePositions position)
|
|
{
|
|
return (position & RelativeMousePositions.Right) == RelativeMousePositions.Right;
|
|
}
|
|
|
|
private static bool IsMouseAbove(RelativeMousePositions position)
|
|
{
|
|
return (position & RelativeMousePositions.Above) == RelativeMousePositions.Above;
|
|
}
|
|
|
|
private static bool IsMouseBelow(RelativeMousePositions position)
|
|
{
|
|
return (position & RelativeMousePositions.Below) == RelativeMousePositions.Below;
|
|
}
|
|
|
|
private static bool IsMouseToLeftOrRightOnly(RelativeMousePositions position)
|
|
{
|
|
return (position == RelativeMousePositions.Left) || (position == RelativeMousePositions.Right);
|
|
}
|
|
|
|
private static bool IsMouseInCorner(RelativeMousePositions position)
|
|
{
|
|
return (position != RelativeMousePositions.Over) &&
|
|
(position != RelativeMousePositions.Above) &&
|
|
(position != RelativeMousePositions.Below) &&
|
|
(position != RelativeMousePositions.Left) &&
|
|
(position != RelativeMousePositions.Right);
|
|
}
|
|
|
|
[Flags]
|
|
private enum RelativeMousePositions
|
|
{
|
|
Over = 0x00,
|
|
Above = 0x01,
|
|
Below = 0x02,
|
|
Left = 0x04,
|
|
Right = 0x08,
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Automation
|
|
|
|
protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer()
|
|
{
|
|
return new EGC.DataGridAutomationPeer(this);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cell Info
|
|
|
|
private EGC.DataGridCell TryFindCell(EGC.DataGridCellInfo info)
|
|
{
|
|
// Does not de-virtualize cells
|
|
return TryFindCell(info.Item, info.Column);
|
|
}
|
|
|
|
internal EGC.DataGridCell TryFindCell(object item, EGC.DataGridColumn column)
|
|
{
|
|
// Does not de-virtualize cells
|
|
EGC.DataGridRow row = (EGC.DataGridRow)ItemContainerGenerator.ContainerFromItem(item);
|
|
int columnIndex = _columns.IndexOf(column);
|
|
if ((row != null) && (columnIndex >= 0))
|
|
{
|
|
return row.TryGetCell(columnIndex);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Auto Sort
|
|
|
|
/// <summary>
|
|
/// Dependecy property for CanUserSortColumns Property
|
|
/// </summary>
|
|
public static readonly DependencyProperty CanUserSortColumnsProperty =
|
|
DependencyProperty.Register(
|
|
"CanUserSortColumns",
|
|
typeof(bool),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(true));
|
|
|
|
/// <summary>
|
|
/// The property which determines whether the datagrid can be sorted by
|
|
/// cells in the columns or not
|
|
/// </summary>
|
|
public bool CanUserSortColumns
|
|
{
|
|
get { return (bool)GetValue(CanUserSortColumnsProperty); }
|
|
set { SetValue(CanUserSortColumnsProperty, value); }
|
|
}
|
|
|
|
public event DataGridSortingEventHandler Sorting;
|
|
|
|
/// <summary>
|
|
/// Protected method which raises the sorting event and does default sort
|
|
/// </summary>
|
|
/// <param name="eventArgs"></param>
|
|
protected virtual void OnSorting(EGC.DataGridSortingEventArgs eventArgs)
|
|
{
|
|
eventArgs.Handled = false;
|
|
if (Sorting != null)
|
|
{
|
|
Sorting(this, eventArgs);
|
|
}
|
|
|
|
if (!eventArgs.Handled)
|
|
{
|
|
DefaultSort(
|
|
eventArgs.Column,
|
|
/* clearExistinSortDescriptions */
|
|
(Keyboard.Modifiers & ModifierKeys.Shift) != ModifierKeys.Shift);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method to perform sorting on datagrid
|
|
/// </summary>
|
|
/// <param name="sortColumn"></param>
|
|
internal void PerformSort(EGC.DataGridColumn sortColumn)
|
|
{
|
|
Debug.Assert(sortColumn != null, "column should not be null");
|
|
|
|
if (!CanUserSortColumns || !sortColumn.CanUserSort)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (CommitAnyEdit())
|
|
{
|
|
PrepareForSort(sortColumn);
|
|
|
|
EGC.DataGridSortingEventArgs eventArgs = new EGC.DataGridSortingEventArgs(sortColumn);
|
|
OnSorting(eventArgs);
|
|
|
|
if (Items.NeedsRefresh)
|
|
{
|
|
try
|
|
{
|
|
Items.Refresh();
|
|
}
|
|
catch (InvalidOperationException invalidOperationException)
|
|
{
|
|
Items.SortDescriptions.Clear();
|
|
throw new InvalidOperationException(SR.Get(SRID.DataGrid_ProbableInvalidSortDescription), invalidOperationException);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the sort directions for all the columns except the column to be sorted upon
|
|
/// </summary>
|
|
/// <param name="sortColumn"></param>
|
|
private void PrepareForSort(EGC.DataGridColumn sortColumn)
|
|
{
|
|
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Columns != null)
|
|
{
|
|
foreach (EGC.DataGridColumn column in Columns)
|
|
{
|
|
if (column != sortColumn)
|
|
{
|
|
column.SortDirection = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines the sort direction and sort property name and adds a sort
|
|
/// description to the Items>SortDescriptions Collection. Clears all the
|
|
/// existing sort descriptions.
|
|
/// </summary>
|
|
/// <param name="column"></param>
|
|
/// <param name="clearExistingSortDescriptions"></param>
|
|
private void DefaultSort(EGC.DataGridColumn column, bool clearExistingSortDescriptions)
|
|
{
|
|
ListSortDirection sortDirection = ListSortDirection.Ascending;
|
|
Nullable<ListSortDirection> currentSortDirection = column.SortDirection;
|
|
if (currentSortDirection.HasValue &&
|
|
currentSortDirection.Value == ListSortDirection.Ascending)
|
|
{
|
|
sortDirection = ListSortDirection.Descending;
|
|
}
|
|
|
|
string sortPropertyName = column.SortMemberPath;
|
|
if (!string.IsNullOrEmpty(sortPropertyName))
|
|
{
|
|
int descriptorIndex = -1;
|
|
if (clearExistingSortDescriptions)
|
|
{
|
|
// clear the sortdesriptions collection
|
|
Items.SortDescriptions.Clear();
|
|
}
|
|
else
|
|
{
|
|
// get the index of existing descriptor to replace it
|
|
for (int i = 0; i < Items.SortDescriptions.Count; i++)
|
|
{
|
|
if (string.Compare(Items.SortDescriptions[i].PropertyName, sortPropertyName, StringComparison.Ordinal) == 0 &&
|
|
(GroupingSortDescriptionIndices == null ||
|
|
!GroupingSortDescriptionIndices.Contains(i)))
|
|
{
|
|
descriptorIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
SortDescription sortDescription = new SortDescription(sortPropertyName, sortDirection);
|
|
try
|
|
{
|
|
if (descriptorIndex >= 0)
|
|
{
|
|
Items.SortDescriptions[descriptorIndex] = sortDescription;
|
|
}
|
|
else
|
|
{
|
|
Items.SortDescriptions.Add(sortDescription);
|
|
}
|
|
|
|
if (clearExistingSortDescriptions || !_sortingStarted)
|
|
{
|
|
RegenerateGroupingSortDescriptions();
|
|
_sortingStarted = true;
|
|
}
|
|
}
|
|
catch (InvalidOperationException invalidOperationException)
|
|
{
|
|
Items.SortDescriptions.Clear();
|
|
throw new InvalidOperationException(SR.Get(SRID.DataGrid_InvalidSortDescription), invalidOperationException);
|
|
}
|
|
|
|
column.SortDirection = sortDirection;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// List which holds all the indices of SortDescriptions which were
|
|
/// added for the sake of GroupDescriptions
|
|
/// </summary>
|
|
private List<int> GroupingSortDescriptionIndices
|
|
{
|
|
get
|
|
{
|
|
return _groupingSortDescriptionIndices;
|
|
}
|
|
|
|
set
|
|
{
|
|
_groupingSortDescriptionIndices = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// SortDescription collection changed listener. Ensures that GroupingSortDescriptionIndices
|
|
/// is in sync with SortDescriptions.
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
private void OnItemsSortDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (_ignoreSortDescriptionsChange || GroupingSortDescriptionIndices == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (e.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
Debug.Assert(e.NewItems.Count == 1, "SortDescriptionCollection should handle one element at a time");
|
|
for (int i = 0, count = GroupingSortDescriptionIndices.Count; i < count; i++)
|
|
{
|
|
if (GroupingSortDescriptionIndices[i] >= e.NewStartingIndex)
|
|
{
|
|
GroupingSortDescriptionIndices[i]++;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case NotifyCollectionChangedAction.Remove:
|
|
Debug.Assert(e.OldItems.Count == 1, "SortDescriptionCollection should handle one element at a time");
|
|
for (int i = 0, count = GroupingSortDescriptionIndices.Count; i < count; i++)
|
|
{
|
|
if (GroupingSortDescriptionIndices[i] > e.OldStartingIndex)
|
|
{
|
|
GroupingSortDescriptionIndices[i]--;
|
|
}
|
|
else if (GroupingSortDescriptionIndices[i] == e.OldStartingIndex)
|
|
{
|
|
GroupingSortDescriptionIndices.RemoveAt(i);
|
|
i--;
|
|
count--;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case NotifyCollectionChangedAction.Move:
|
|
// SortDescriptionCollection doesnt support move, atleast as an atomic operation. Hence Do nothing.
|
|
break;
|
|
case NotifyCollectionChangedAction.Replace:
|
|
Debug.Assert(e.OldItems.Count == 1 && e.NewItems.Count == 1, "SortDescriptionCollection should handle one element at a time");
|
|
GroupingSortDescriptionIndices.Remove(e.OldStartingIndex);
|
|
break;
|
|
case NotifyCollectionChangedAction.Reset:
|
|
GroupingSortDescriptionIndices.Clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method to remove all the SortDescriptions which were added based on GroupDescriptions
|
|
/// </summary>
|
|
private void RemoveGroupingSortDescriptions()
|
|
{
|
|
if (GroupingSortDescriptionIndices == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool originalIgnoreSortDescriptionChanges = _ignoreSortDescriptionsChange;
|
|
_ignoreSortDescriptionsChange = true;
|
|
try
|
|
{
|
|
for (int i = 0, count = GroupingSortDescriptionIndices.Count; i < count; i++)
|
|
{
|
|
Items.SortDescriptions.RemoveAt(GroupingSortDescriptionIndices[i] - i);
|
|
}
|
|
|
|
GroupingSortDescriptionIndices.Clear();
|
|
}
|
|
finally
|
|
{
|
|
_ignoreSortDescriptionsChange = originalIgnoreSortDescriptionChanges;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which determines if one can create a SortDescription out of
|
|
/// a GroupDescription.
|
|
/// </summary>
|
|
/// <param name="propertyGroupDescription"></param>
|
|
/// <returns></returns>
|
|
private static bool CanConvertToSortDescription(PropertyGroupDescription propertyGroupDescription)
|
|
{
|
|
if (propertyGroupDescription != null &&
|
|
propertyGroupDescription.Converter == null &&
|
|
propertyGroupDescription.StringComparison == StringComparison.Ordinal)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method to add SortDescriptions based on GroupDescriptions.
|
|
/// Only PropertGroupDescriptions with no ValueConverter and with
|
|
/// Oridinal comparison are considered suitable.
|
|
/// </summary>
|
|
private void AddGroupingSortDescriptions()
|
|
{
|
|
bool originalIgnoreSortDescriptionChanges = _ignoreSortDescriptionsChange;
|
|
_ignoreSortDescriptionsChange = true;
|
|
try
|
|
{
|
|
int insertIndex = 0;
|
|
foreach (GroupDescription groupDescription in Items.GroupDescriptions)
|
|
{
|
|
PropertyGroupDescription propertyGroupDescription = groupDescription as PropertyGroupDescription;
|
|
if (CanConvertToSortDescription(propertyGroupDescription))
|
|
{
|
|
SortDescription sortDescription = new SortDescription(propertyGroupDescription.PropertyName, ListSortDirection.Ascending);
|
|
Items.SortDescriptions.Insert(insertIndex, sortDescription);
|
|
if (GroupingSortDescriptionIndices == null)
|
|
{
|
|
GroupingSortDescriptionIndices = new List<int>();
|
|
}
|
|
|
|
GroupingSortDescriptionIndices.Add(insertIndex++);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_ignoreSortDescriptionsChange = originalIgnoreSortDescriptionChanges;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method to regenrated the SortDescriptions based on the GroupDescriptions
|
|
/// </summary>
|
|
private void RegenerateGroupingSortDescriptions()
|
|
{
|
|
RemoveGroupingSortDescriptions();
|
|
AddGroupingSortDescriptions();
|
|
}
|
|
|
|
/// <summary>
|
|
/// CollectionChanged listener for GroupDescriptions of DataGrid.
|
|
/// Regenerates Grouping based sort descriptions is required.
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
private void OnItemsGroupDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (!_sortingStarted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (e.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
Debug.Assert(e.NewItems.Count == 1, "GroupDescriptionCollection should handle one element at a time");
|
|
if (CanConvertToSortDescription(e.NewItems[0] as PropertyGroupDescription))
|
|
{
|
|
RegenerateGroupingSortDescriptions();
|
|
}
|
|
|
|
break;
|
|
case NotifyCollectionChangedAction.Remove:
|
|
Debug.Assert(e.OldItems.Count == 1, "GroupDescriptionCollection should handle one element at a time");
|
|
if (CanConvertToSortDescription(e.OldItems[0] as PropertyGroupDescription))
|
|
{
|
|
RegenerateGroupingSortDescriptions();
|
|
}
|
|
|
|
break;
|
|
case NotifyCollectionChangedAction.Move:
|
|
// Do Nothing
|
|
break;
|
|
case NotifyCollectionChangedAction.Replace:
|
|
Debug.Assert(e.OldItems.Count == 1 && e.NewItems.Count == 1, "GroupDescriptionCollection should handle one element at a time");
|
|
if (CanConvertToSortDescription(e.OldItems[0] as PropertyGroupDescription) ||
|
|
CanConvertToSortDescription(e.NewItems[0] as PropertyGroupDescription))
|
|
{
|
|
RegenerateGroupingSortDescriptions();
|
|
}
|
|
|
|
break;
|
|
case NotifyCollectionChangedAction.Reset:
|
|
RemoveGroupingSortDescriptions();
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Column Auto Generation
|
|
|
|
/// <summary>
|
|
/// This event will be raised whenever auto generation of columns gets completed
|
|
/// </summary>
|
|
public event EventHandler AutoGeneratedColumns;
|
|
|
|
/// <summary>
|
|
/// This event will be raised for each column getting auto generated
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridAutoGeneratingColumnEventArgs> AutoGeneratingColumn;
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the AutoGenerateColumns property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty AutoGenerateColumnsProperty =
|
|
DependencyProperty.Register("AutoGenerateColumns", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnAutoGenerateColumnsPropertyChanged)));
|
|
|
|
/// <summary>
|
|
/// The property which determines whether the columns are to be auto generated or not.
|
|
/// Setting of the property actually generates or deletes columns.
|
|
/// </summary>
|
|
public bool AutoGenerateColumns
|
|
{
|
|
get { return (bool)GetValue(AutoGenerateColumnsProperty); }
|
|
set { SetValue(AutoGenerateColumnsProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The polumorphic method which raises the AutoGeneratedColumns event
|
|
/// </summary>
|
|
/// <param name="e"></param>
|
|
protected virtual void OnAutoGeneratedColumns(EventArgs e)
|
|
{
|
|
if (AutoGeneratedColumns != null)
|
|
{
|
|
AutoGeneratedColumns(this, e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The polymorphic method which raises the AutoGeneratingColumn event
|
|
/// </summary>
|
|
/// <param name="e"></param>
|
|
protected virtual void OnAutoGeneratingColumn(EGC.DataGridAutoGeneratingColumnEventArgs e)
|
|
{
|
|
if (AutoGeneratingColumn != null)
|
|
{
|
|
AutoGeneratingColumn(this, e);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines the desired size of the control given a constraint.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// On the first measure:
|
|
/// - Performs auto-generation of columns if needed.
|
|
/// - Coerces CanUserAddRows and CanUserDeleteRows.
|
|
/// - Updates the NewItemPlaceholder.
|
|
/// </remarks>
|
|
/// <param name="availableSize">The available space.</param>
|
|
/// <returns>The desired size of the control.</returns>
|
|
protected override Size MeasureOverride(Size availableSize)
|
|
{
|
|
if (_measureNeverInvoked)
|
|
{
|
|
_measureNeverInvoked = false;
|
|
if (AutoGenerateColumns)
|
|
{
|
|
AddAutoColumns();
|
|
}
|
|
|
|
InternalColumns.InitializeDisplayIndexMap();
|
|
|
|
// FrozenColumns rely on column DisplayIndex
|
|
CoerceValue(FrozenColumnCountProperty);
|
|
|
|
// These properties rely on a variety of properties. This is necessary since
|
|
// our default (true) is actually incorrect initially (when ItemsSource is null).
|
|
// So, we delay to this point, in case ItemsSource is never set, to coerce them
|
|
// to their correct values. If ItemsSource did change, then they will have their
|
|
// correct values already and this is extra work.
|
|
CoerceValue(CanUserAddRowsProperty);
|
|
CoerceValue(CanUserDeleteRowsProperty);
|
|
|
|
// We need to call this in case CanUserAddRows has remained true (the default value)
|
|
// since startup and no one has set the placeholder position.
|
|
UpdateNewItemPlaceholder(/* isAddingNewItem = */ false);
|
|
}
|
|
|
|
return base.MeasureOverride(availableSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Coercion callback for ItemsSource property
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// SortDescriptions and GroupDescriptions are supposed to be
|
|
/// cleared in PropertyChangedCallback or OnItemsSourceChanged
|
|
/// virtual. But it seems that the SortDescriptions are applied
|
|
/// to the new CollectionView due to new ItemsSource in
|
|
/// PropertyChangedCallback of base class (which would execute
|
|
/// before PropertyChangedCallback of this class) and before calling
|
|
/// OnItemsSourceChanged virtual. Hence handling it in Coercion callback.
|
|
/// </remarks>
|
|
private static object OnCoerceItemsSourceProperty(DependencyObject d, object baseValue)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
if (baseValue != dataGrid._cachedItemsSource && dataGrid._cachedItemsSource != null)
|
|
{
|
|
dataGrid.Items.SortDescriptions.Clear();
|
|
dataGrid.Items.GroupDescriptions.Clear();
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The polymorphic method which gets called whenever the ItemsSource gets changed.
|
|
/// We regenerate columns if required when ItemsSource gets changed.
|
|
/// </summary>
|
|
/// <param name="oldValue"></param>
|
|
/// <param name="newValue"></param>
|
|
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
|
|
{
|
|
base.OnItemsSourceChanged(oldValue, newValue);
|
|
|
|
// ItemsControl calls a ClearValue on ItemsSource property
|
|
// whenever it is set to null. So Coercion is not called
|
|
// in such case. Hence clearing the SortDescriptions and
|
|
// GroupDescriptions here when new value is null.
|
|
if (newValue == null)
|
|
{
|
|
Items.SortDescriptions.Clear();
|
|
Items.GroupDescriptions.Clear();
|
|
}
|
|
|
|
_cachedItemsSource = newValue;
|
|
|
|
INotifyCollectionChanged oldNotifyCollection = oldValue as INotifyCollectionChanged;
|
|
if (oldNotifyCollection != null && DeferAutoGeneration)
|
|
{
|
|
oldNotifyCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(OnItemsSourceCollectionChanged);
|
|
}
|
|
|
|
using (UpdateSelectedCells())
|
|
{
|
|
// Selector will try to maintain the previous row selection.
|
|
// Keep SelectedCells in sync.
|
|
_selectedCells.RestoreOnlyFullRows(SelectedItems);
|
|
}
|
|
|
|
if (AutoGenerateColumns == true)
|
|
{
|
|
RegenerateAutoColumns();
|
|
}
|
|
|
|
InternalColumns.InvalidateColumnWidthsComputation();
|
|
|
|
CoerceValue(CanUserAddRowsProperty);
|
|
CoerceValue(CanUserDeleteRowsProperty);
|
|
|
|
ResetRowHeaderActualWidth();
|
|
|
|
UpdateNewItemPlaceholder(/* isAddingNewItem = */ false);
|
|
|
|
HasCellValidationError = false;
|
|
HasRowValidationError = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Private property to hook and unhook collection changed event on ItemsSources CollectionChanged
|
|
/// event when ever _deferAutoGeneration flag changes.
|
|
/// </summary>
|
|
private bool DeferAutoGeneration
|
|
{
|
|
get
|
|
{
|
|
return _deferAutoGeneration;
|
|
}
|
|
|
|
set
|
|
{
|
|
bool oldValue = _deferAutoGeneration;
|
|
_deferAutoGeneration = value;
|
|
if (oldValue != value)
|
|
{
|
|
INotifyCollectionChanged notifyCollection = ItemsSource as INotifyCollectionChanged;
|
|
if (notifyCollection != null)
|
|
{
|
|
if (value)
|
|
{
|
|
notifyCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(OnItemsSourceCollectionChanged);
|
|
}
|
|
else
|
|
{
|
|
notifyCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(OnItemsSourceCollectionChanged);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The event listener to ItemsSource collection changed event which performs deffered auto generation
|
|
/// and also unhooks it self as needed.
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
private void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
|
{
|
|
AddAutoColumns();
|
|
DeferAutoGeneration = false;
|
|
}
|
|
else if ((e.Action == NotifyCollectionChangedAction.Remove) || (e.Action == NotifyCollectionChangedAction.Replace))
|
|
{
|
|
if (HasRowValidationError || HasCellValidationError)
|
|
{
|
|
foreach (object item in e.OldItems)
|
|
{
|
|
if (IsAddingOrEditingRowItem(item))
|
|
{
|
|
HasRowValidationError = false;
|
|
HasCellValidationError = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (e.Action == NotifyCollectionChangedAction.Reset)
|
|
{
|
|
ResetRowHeaderActualWidth();
|
|
HasRowValidationError = false;
|
|
HasCellValidationError = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method which generated auto columns and adds to the data grid.
|
|
/// </summary>
|
|
private void AddAutoColumns()
|
|
{
|
|
if (ItemsSource != null &&
|
|
ItemsSource is INotifyCollectionChanged &&
|
|
DataItemsCount == 0)
|
|
{
|
|
// do deferred generation
|
|
DeferAutoGeneration = true;
|
|
}
|
|
else if (!_measureNeverInvoked)
|
|
{
|
|
EGC.DataGrid.GenerateColumns(
|
|
(IItemProperties)Items,
|
|
this,
|
|
null);
|
|
|
|
OnAutoGeneratedColumns(EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method which deletes all the auto generated columns.
|
|
/// </summary>
|
|
private void DeleteAutoColumns()
|
|
{
|
|
if (!DeferAutoGeneration && !_measureNeverInvoked)
|
|
{
|
|
for (int columnIndex = Columns.Count - 1; columnIndex >= 0; --columnIndex)
|
|
{
|
|
if (Columns[columnIndex].IsAutoGenerated)
|
|
{
|
|
Columns.RemoveAt(columnIndex);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DeferAutoGeneration = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method which regenerates the columns for the datagrid
|
|
/// </summary>
|
|
private void RegenerateAutoColumns()
|
|
{
|
|
DeleteAutoColumns();
|
|
AddAutoColumns();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which generates columns for a given IItemProperties
|
|
/// </summary>
|
|
/// <param name="iItemProperties"></param>
|
|
/// <returns></returns>
|
|
public static Collection<EGC.DataGridColumn> GenerateColumns(IItemProperties itemProperties)
|
|
{
|
|
if (itemProperties == null)
|
|
{
|
|
throw new ArgumentNullException("itemProperties");
|
|
}
|
|
|
|
Collection<EGC.DataGridColumn> columnCollection = new Collection<EGC.DataGridColumn>();
|
|
EGC.DataGrid.GenerateColumns(
|
|
itemProperties,
|
|
null,
|
|
columnCollection);
|
|
return columnCollection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which generates columns for a given IItemProperties and adds
|
|
/// them either to a datagrid or to a collection of columns as specified by the flag.
|
|
/// </summary>
|
|
/// <param name="dataGrid"></param>
|
|
/// <param name="iItemProperties"></param>
|
|
/// <param name="columnCollection"></param>
|
|
private static void GenerateColumns(
|
|
IItemProperties iItemProperties,
|
|
EGC.DataGrid dataGrid,
|
|
Collection<EGC.DataGridColumn> columnCollection)
|
|
{
|
|
Debug.Assert(iItemProperties != null, "iItemProperties should not be null");
|
|
Debug.Assert(dataGrid != null || columnCollection != null, "Both dataGrid and columnCollection cannot not be null at the same time");
|
|
|
|
ReadOnlyCollection<ItemPropertyInfo> itemProperties = iItemProperties.ItemProperties;
|
|
|
|
if (itemProperties != null &&
|
|
itemProperties.Count > 0)
|
|
{
|
|
foreach (ItemPropertyInfo itemProperty in itemProperties)
|
|
{
|
|
EGC.DataGridColumn dataGridColumn = EGC.DataGridColumn.CreateDefaultColumn(itemProperty);
|
|
|
|
if (dataGrid != null)
|
|
{
|
|
// AutoGeneratingColumn event is raised before generating and adding column to datagrid
|
|
// and the column returned by the event handler is used instead of the original column.
|
|
EGC.DataGridAutoGeneratingColumnEventArgs eventArgs = new EGC.DataGridAutoGeneratingColumnEventArgs(dataGridColumn, itemProperty);
|
|
dataGrid.OnAutoGeneratingColumn(eventArgs);
|
|
|
|
if (!eventArgs.Cancel && eventArgs.Column != null)
|
|
{
|
|
eventArgs.Column.IsAutoGenerated = true;
|
|
dataGrid.Columns.Add(eventArgs.Column);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
columnCollection.Add(dataGridColumn);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The event listener which listens to the change in the AutoGenerateColumns flag
|
|
/// </summary>
|
|
/// <param name="d"></param>
|
|
/// <param name="e"></param>
|
|
private static void OnAutoGenerateColumnsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
bool newValue = (bool)e.NewValue;
|
|
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
|
|
if (newValue)
|
|
{
|
|
dataGrid.AddAutoColumns();
|
|
}
|
|
else
|
|
{
|
|
dataGrid.DeleteAutoColumns();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Frozen Columns
|
|
|
|
/// <summary>
|
|
/// Dependency Property fro FrozenColumnCount Property
|
|
/// </summary>
|
|
public static readonly DependencyProperty FrozenColumnCountProperty =
|
|
DependencyProperty.Register(
|
|
"FrozenColumnCount",
|
|
typeof(int),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(0, new PropertyChangedCallback(OnFrozenColumnCountPropertyChanged), new CoerceValueCallback(OnCoerceFrozenColumnCount)),
|
|
new ValidateValueCallback(ValidateFrozenColumnCount));
|
|
|
|
/// <summary>
|
|
/// Property which determines the number of columns which are frozen from the beginning in order of display
|
|
/// </summary>
|
|
public int FrozenColumnCount
|
|
{
|
|
get { return (int)GetValue(FrozenColumnCountProperty); }
|
|
set { SetValue(FrozenColumnCountProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Coercion call back for FrozenColumnCount property, which ensures that it is never more that column count
|
|
/// </summary>
|
|
/// <param name="d"></param>
|
|
/// <param name="baseValue"></param>
|
|
/// <returns></returns>
|
|
private static object OnCoerceFrozenColumnCount(DependencyObject d, object baseValue)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
int frozenColumnCount = (int)baseValue;
|
|
|
|
if (frozenColumnCount > dataGrid.Columns.Count)
|
|
{
|
|
return dataGrid.Columns.Count;
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property changed callback fro FrozenColumnCount
|
|
/// </summary>
|
|
/// <param name="d"></param>
|
|
/// <param name="e"></param>
|
|
private static void OnFrozenColumnCountPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnCollection | NotificationTarget.ColumnHeadersPresenter | NotificationTarget.CellsPresenter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validation call back for frozen column count
|
|
/// </summary>
|
|
/// <param name="value"></param>
|
|
/// <returns></returns>
|
|
private static bool ValidateFrozenColumnCount(object value)
|
|
{
|
|
int frozenCount = (int)value;
|
|
return frozenCount >= 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dependency Property key for NonFrozenColumnsViewportHorizontalOffset Property
|
|
/// </summary>
|
|
private static readonly DependencyPropertyKey NonFrozenColumnsViewportHorizontalOffsetPropertyKey =
|
|
DependencyProperty.RegisterReadOnly(
|
|
"NonFrozenColumnsViewportHorizontalOffset",
|
|
typeof(double),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(0.0));
|
|
|
|
/// <summary>
|
|
/// Dependency property for NonFrozenColumnsViewportHorizontalOffset Property
|
|
/// </summary>
|
|
public static readonly DependencyProperty NonFrozenColumnsViewportHorizontalOffsetProperty = NonFrozenColumnsViewportHorizontalOffsetPropertyKey.DependencyProperty;
|
|
|
|
/// <summary>
|
|
/// Property which gets/sets the start x coordinate of non frozen columns in view port
|
|
/// </summary>
|
|
public double NonFrozenColumnsViewportHorizontalOffset
|
|
{
|
|
get
|
|
{
|
|
return (double)GetValue(NonFrozenColumnsViewportHorizontalOffsetProperty);
|
|
}
|
|
|
|
internal set
|
|
{
|
|
SetValue(NonFrozenColumnsViewportHorizontalOffsetPropertyKey, value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Override of OnApplyTemplate which clear the scroll host member
|
|
/// </summary>
|
|
public override void OnApplyTemplate()
|
|
{
|
|
CleanUpInternalScrollControls();
|
|
base.OnApplyTemplate();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Container Virtualization
|
|
|
|
/// <summary>
|
|
/// Property which determines if row virtualization is enabled or disabled
|
|
/// </summary>
|
|
public bool EnableRowVirtualization
|
|
{
|
|
get { return (bool)GetValue(EnableRowVirtualizationProperty); }
|
|
set { SetValue(EnableRowVirtualizationProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dependency property for EnableRowVirtualization
|
|
/// </summary>
|
|
public static readonly DependencyProperty EnableRowVirtualizationProperty = DependencyProperty.Register(
|
|
"EnableRowVirtualization",
|
|
typeof(bool),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnEnableRowVirtualizationChanged)));
|
|
|
|
/// <summary>
|
|
/// Property changed callback for EnableRowVirtualization.
|
|
/// Keeps VirtualizingStackPanel.IsVirtualizingProperty in sync.
|
|
/// </summary>
|
|
private static void OnEnableRowVirtualizationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
EGC.DataGrid dataGrid = (EGC.DataGrid)d;
|
|
dataGrid.CoerceValue(VirtualizingStackPanel.IsVirtualizingProperty);
|
|
|
|
Panel itemsHost = dataGrid.InternalItemsHost;
|
|
if (itemsHost != null)
|
|
{
|
|
itemsHost.InvalidateMeasure();
|
|
itemsHost.InvalidateArrange();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Coercion callback for VirtualizingStackPanel.IsVirtualizingProperty
|
|
/// </summary>
|
|
private static object OnCoerceIsVirtualizingProperty(DependencyObject d, object baseValue)
|
|
{
|
|
if (!EGC.DataGridHelper.IsDefaultValue(d, EGC.DataGrid.EnableRowVirtualizationProperty))
|
|
{
|
|
return d.GetValue(EGC.DataGrid.EnableRowVirtualizationProperty);
|
|
}
|
|
|
|
return baseValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property which determines if column virtualization is enabled or disabled
|
|
/// </summary>
|
|
public bool EnableColumnVirtualization
|
|
{
|
|
get { return (bool)GetValue(EnableColumnVirtualizationProperty); }
|
|
set { SetValue(EnableColumnVirtualizationProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dependency property for EnableColumnVirtualization
|
|
/// </summary>
|
|
public static readonly DependencyProperty EnableColumnVirtualizationProperty = DependencyProperty.Register(
|
|
"EnableColumnVirtualization",
|
|
typeof(bool),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnEnableColumnVirtualizationChanged)));
|
|
|
|
/// <summary>
|
|
/// Property changed callback for EnableColumnVirtualization.
|
|
/// Gets VirtualizingStackPanel.IsVirtualizingProperty for cells presenter and
|
|
/// headers presenter in sync.
|
|
/// </summary>
|
|
private static void OnEnableColumnVirtualizationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.CellsPresenter | NotificationTarget.ColumnHeadersPresenter | NotificationTarget.ColumnCollection);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Column Reordering
|
|
|
|
/// <summary>
|
|
/// Dependency Property for CanUserReorderColumns Property
|
|
/// </summary>
|
|
public static readonly DependencyProperty CanUserReorderColumnsProperty =
|
|
DependencyProperty.Register("CanUserReorderColumns", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true));
|
|
|
|
/// <summary>
|
|
/// The property which determines if an end user can re-order columns or not.
|
|
/// </summary>
|
|
public bool CanUserReorderColumns
|
|
{
|
|
get { return (bool)GetValue(CanUserReorderColumnsProperty); }
|
|
set { SetValue(CanUserReorderColumnsProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dependency Property for DragIndicatorStyle property
|
|
/// </summary>
|
|
public static readonly DependencyProperty DragIndicatorStyleProperty =
|
|
DependencyProperty.Register("DragIndicatorStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, OnNotifyColumnPropertyChanged));
|
|
|
|
/// <summary>
|
|
/// The style property which would be applied on the column header drag indicator
|
|
/// </summary>
|
|
public Style DragIndicatorStyle
|
|
{
|
|
get { return (Style)GetValue(DragIndicatorStyleProperty); }
|
|
set { SetValue(DragIndicatorStyleProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dependency Property for DropLocationIndicatorStyle property
|
|
/// </summary>
|
|
public static readonly DependencyProperty DropLocationIndicatorStyleProperty =
|
|
DependencyProperty.Register("DropLocationIndicatorStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null));
|
|
|
|
/// <summary>
|
|
/// The style property which would be applied on the column header drop location indicator.
|
|
/// </summary>
|
|
public Style DropLocationIndicatorStyle
|
|
{
|
|
get { return (Style)GetValue(DropLocationIndicatorStyleProperty); }
|
|
set { SetValue(DropLocationIndicatorStyleProperty, value); }
|
|
}
|
|
|
|
public event EventHandler<EGC.DataGridColumnReorderingEventArgs> ColumnReordering;
|
|
|
|
public event EventHandler<DragStartedEventArgs> ColumnHeaderDragStarted;
|
|
|
|
public event EventHandler<DragDeltaEventArgs> ColumnHeaderDragDelta;
|
|
|
|
public event EventHandler<DragCompletedEventArgs> ColumnHeaderDragCompleted;
|
|
|
|
public event EventHandler<EGC.DataGridColumnEventArgs> ColumnReordered;
|
|
|
|
protected internal virtual void OnColumnHeaderDragStarted(DragStartedEventArgs e)
|
|
{
|
|
if (ColumnHeaderDragStarted != null)
|
|
{
|
|
ColumnHeaderDragStarted(this, e);
|
|
}
|
|
}
|
|
|
|
protected internal virtual void OnColumnReordering(EGC.DataGridColumnReorderingEventArgs e)
|
|
{
|
|
if (ColumnReordering != null)
|
|
{
|
|
ColumnReordering(this, e);
|
|
}
|
|
}
|
|
|
|
protected internal virtual void OnColumnHeaderDragDelta(DragDeltaEventArgs e)
|
|
{
|
|
if (ColumnHeaderDragDelta != null)
|
|
{
|
|
ColumnHeaderDragDelta(this, e);
|
|
}
|
|
}
|
|
|
|
protected internal virtual void OnColumnHeaderDragCompleted(DragCompletedEventArgs e)
|
|
{
|
|
if (ColumnHeaderDragCompleted != null)
|
|
{
|
|
ColumnHeaderDragCompleted(this, e);
|
|
}
|
|
}
|
|
|
|
protected internal virtual void OnColumnReordered(EGC.DataGridColumnEventArgs e)
|
|
{
|
|
if (ColumnReordered != null)
|
|
{
|
|
ColumnReordered(this, e);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Clipboard Copy
|
|
|
|
/// <summary>
|
|
/// The DependencyProperty that represents the ClipboardCopyMode property.
|
|
/// </summary>
|
|
public static readonly DependencyProperty ClipboardCopyModeProperty =
|
|
DependencyProperty.Register("ClipboardCopyMode", typeof(EGC.DataGridClipboardCopyMode), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridClipboardCopyMode.ExcludeHeader, new PropertyChangedCallback(OnClipboardCopyModeChanged)));
|
|
|
|
private static void OnClipboardCopyModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
// The Copy command needs to have CanExecute run
|
|
CommandManager.InvalidateRequerySuggested();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The property which determines how DataGrid content is copied to the Clipboard.
|
|
/// </summary>
|
|
public EGC.DataGridClipboardCopyMode ClipboardCopyMode
|
|
{
|
|
get { return (EGC.DataGridClipboardCopyMode)GetValue(ClipboardCopyModeProperty); }
|
|
set { SetValue(ClipboardCopyModeProperty, value); }
|
|
}
|
|
|
|
private static void OnCanExecuteCopy(object target, CanExecuteRoutedEventArgs args)
|
|
{
|
|
((EGC.DataGrid)target).OnCanExecuteCopy(args);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This virtual method is called when ApplicationCommands.Copy command query its state.
|
|
/// </summary>
|
|
/// <param name="args"></param>
|
|
protected virtual void OnCanExecuteCopy(CanExecuteRoutedEventArgs args)
|
|
{
|
|
args.CanExecute = ClipboardCopyMode != EGC.DataGridClipboardCopyMode.None && _selectedCells.Count > 0;
|
|
args.Handled = true;
|
|
}
|
|
|
|
private static void OnExecutedCopy(object target, ExecutedRoutedEventArgs args)
|
|
{
|
|
((EGC.DataGrid)target).OnExecutedCopy(args);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This virtual method is called when ApplicationCommands.Copy command is executed.
|
|
/// </summary>
|
|
/// <param name="args"></param>
|
|
protected virtual void OnExecutedCopy(ExecutedRoutedEventArgs args)
|
|
{
|
|
if (ClipboardCopyMode == EGC.DataGridClipboardCopyMode.None)
|
|
{
|
|
throw new NotSupportedException(SR.Get(SRID.ClipboardCopyMode_Disabled));
|
|
}
|
|
|
|
args.Handled = true;
|
|
|
|
// Supported default formats: Html, Text, UnicodeText and CSV
|
|
Collection<string> formats = new Collection<string>(new string[] { DataFormats.Html, DataFormats.Text, DataFormats.UnicodeText, DataFormats.CommaSeparatedValue });
|
|
Dictionary<string, StringBuilder> dataGridStringBuilders = new Dictionary<string, StringBuilder>(formats.Count);
|
|
foreach (string format in formats)
|
|
{
|
|
dataGridStringBuilders[format] = new StringBuilder();
|
|
}
|
|
|
|
int minRowIndex;
|
|
int maxRowIndex;
|
|
int minColumnDisplayIndex;
|
|
int maxColumnDisplayIndex;
|
|
|
|
// Get the bounding box of the selected cells
|
|
if (_selectedCells.GetSelectionRange(out minColumnDisplayIndex, out maxColumnDisplayIndex, out minRowIndex, out maxRowIndex))
|
|
{
|
|
// Add column headers if enabled
|
|
if (ClipboardCopyMode == EGC.DataGridClipboardCopyMode.IncludeHeader)
|
|
{
|
|
EGC.DataGridRowClipboardEventArgs preparingRowClipboardContentEventArgs = new EGC.DataGridRowClipboardEventArgs(null, minColumnDisplayIndex, maxColumnDisplayIndex, true /*IsColumnHeadersRow*/);
|
|
OnCopyingRowClipboardContent(preparingRowClipboardContentEventArgs);
|
|
|
|
foreach (string format in formats)
|
|
{
|
|
dataGridStringBuilders[format].Append(preparingRowClipboardContentEventArgs.FormatClipboardCellValues(format));
|
|
}
|
|
}
|
|
|
|
// Add each selected row
|
|
for (int i = minRowIndex; i <= maxRowIndex; i++)
|
|
{
|
|
object row = Items[i];
|
|
|
|
// Row has a selecion
|
|
if (_selectedCells.Intersects(i))
|
|
{
|
|
EGC.DataGridRowClipboardEventArgs preparingRowClipboardContentEventArgs = new EGC.DataGridRowClipboardEventArgs(row, minColumnDisplayIndex, maxColumnDisplayIndex, false /*IsColumnHeadersRow*/, i);
|
|
OnCopyingRowClipboardContent(preparingRowClipboardContentEventArgs);
|
|
|
|
foreach (string format in formats)
|
|
{
|
|
dataGridStringBuilders[format].Append(preparingRowClipboardContentEventArgs.FormatClipboardCellValues(format));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
EGC.ClipboardHelper.GetClipboardContentForHtml(dataGridStringBuilders[DataFormats.Html]);
|
|
|
|
try
|
|
{
|
|
DataObject dataObject = new DataObject();
|
|
foreach (string format in formats)
|
|
{
|
|
dataObject.SetData(format, dataGridStringBuilders[format].ToString(), false /*autoConvert*/);
|
|
}
|
|
|
|
Clipboard.SetDataObject(dataObject);
|
|
}
|
|
catch (SecurityException)
|
|
{
|
|
// In partial trust we will have a security exception because clipboard operations require elevated permissions
|
|
// Bug: Once the security team fix Clipboard.SetText - we can remove this catch
|
|
// Temp: Use TextBox.Copy to have at least Text format in the clipboard
|
|
TextBox textBox = new TextBox();
|
|
textBox.Text = dataGridStringBuilders[DataFormats.Text].ToString();
|
|
textBox.SelectAll();
|
|
textBox.Copy();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method is called to prepare the clipboard content for each selected row.
|
|
/// If ClipboardCopyMode is set to ClipboardCopyMode, then it is also called to prepare the column headers
|
|
/// </summary>
|
|
/// <param name="args">Contains the necessary information for generating the row clipboard content.</param>
|
|
protected virtual void OnCopyingRowClipboardContent(EGC.DataGridRowClipboardEventArgs args)
|
|
{
|
|
if (args.IsColumnHeadersRow)
|
|
{
|
|
for (int i = args.StartColumnDisplayIndex; i <= args.EndColumnDisplayIndex; i++)
|
|
{
|
|
EGC.DataGridColumn column = ColumnFromDisplayIndex(i);
|
|
if (!column.IsVisible)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
args.ClipboardRowContent.Add(new EGC.DataGridClipboardCellContent(args.Item, column, column.Header));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int rowIndex = args.RowIndexHint;
|
|
if (rowIndex < 0)
|
|
{
|
|
rowIndex = Items.IndexOf(args.Item);
|
|
}
|
|
|
|
// If row has selection
|
|
if (_selectedCells.Intersects(rowIndex))
|
|
{
|
|
for (int i = args.StartColumnDisplayIndex; i <= args.EndColumnDisplayIndex; i++)
|
|
{
|
|
EGC.DataGridColumn column = ColumnFromDisplayIndex(i);
|
|
if (!column.IsVisible)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
object cellValue = null;
|
|
|
|
// Get cell value only if the cell is selected - otherwise leave it null
|
|
if (_selectedCells.Contains(rowIndex, i))
|
|
{
|
|
cellValue = column.OnCopyingCellClipboardContent(args.Item);
|
|
}
|
|
|
|
args.ClipboardRowContent.Add(new EGC.DataGridClipboardCellContent(args.Item, column, cellValue));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Raise the event to give a chance to external listeners to modify row clipboard content (e.ClipboardRow)
|
|
if (CopyingRowClipboardContent != null)
|
|
{
|
|
CopyingRowClipboardContent(this, args);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This event is raised by OnCopyingRowClipboardContent method after the default row content is prepared.
|
|
/// Event listeners can modify or add to the row clipboard content
|
|
/// </summary>
|
|
public event EventHandler<EGC.DataGridRowClipboardEventArgs> CopyingRowClipboardContent;
|
|
#endregion
|
|
|
|
#region Cells Panel Width
|
|
|
|
/// <summary>
|
|
/// Dependency Property for CellsPanelActualWidth property
|
|
/// </summary>
|
|
internal static readonly DependencyProperty CellsPanelActualWidthProperty =
|
|
DependencyProperty.Register(
|
|
"CellsPanelActualWidth",
|
|
typeof(double),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(CellsPanelActualWidthChanged)));
|
|
|
|
/// <summary>
|
|
/// The property which represents the actual width of the cells panel,
|
|
/// to be used by headers presenter
|
|
/// </summary>
|
|
internal double CellsPanelActualWidth
|
|
{
|
|
get
|
|
{
|
|
return (double)GetValue(CellsPanelActualWidthProperty);
|
|
}
|
|
|
|
set
|
|
{
|
|
SetValue(CellsPanelActualWidthProperty, value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property changed callback for CellsPanelActualWidth property
|
|
/// </summary>
|
|
/// <param name="d"></param>
|
|
/// <param name="e"></param>
|
|
private static void CellsPanelActualWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
double oldValue = (double)e.OldValue;
|
|
double newValue = (double)e.NewValue;
|
|
if (!DoubleUtil.AreClose(oldValue, newValue))
|
|
{
|
|
((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnHeadersPresenter);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Column Width Computations
|
|
|
|
/// <summary>
|
|
/// Dependency Property Key for CellsPanelHorizontalOffset property
|
|
/// </summary>
|
|
private static readonly DependencyPropertyKey CellsPanelHorizontalOffsetPropertyKey =
|
|
DependencyProperty.RegisterReadOnly(
|
|
"CellsPanelHorizontalOffset",
|
|
typeof(double),
|
|
typeof(EGC.DataGrid),
|
|
new FrameworkPropertyMetadata(0d, OnNotifyHorizontalOffsetPropertyChanged));
|
|
|
|
/// <summary>
|
|
/// Dependency Property for CellsPanelHorizontalOffset
|
|
/// </summary>
|
|
public static readonly DependencyProperty CellsPanelHorizontalOffsetProperty = CellsPanelHorizontalOffsetPropertyKey.DependencyProperty;
|
|
|
|
/// <summary>
|
|
/// Property which caches the cells panel horizontal offset
|
|
/// </summary>
|
|
public double CellsPanelHorizontalOffset
|
|
{
|
|
get { return (double)GetValue(CellsPanelHorizontalOffsetProperty); }
|
|
private set { SetValue(CellsPanelHorizontalOffsetPropertyKey, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Property which indicates whether a request to
|
|
/// invalidate CellsPanelOffset is already in queue or not.
|
|
/// </summary>
|
|
private bool CellsPanelHorizontalOffsetComputationPending
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which queue a request to dispatcher to
|
|
/// invalidate the cellspanel offset if not already queued
|
|
/// </summary>
|
|
internal void QueueInvalidateCellsPanelHorizontalOffset()
|
|
{
|
|
if (!CellsPanelHorizontalOffsetComputationPending)
|
|
{
|
|
Dispatcher.BeginInvoke(new DispatcherOperationCallback(InvalidateCellsPanelHorizontalOffset), DispatcherPriority.Loaded, this);
|
|
CellsPanelHorizontalOffsetComputationPending = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispatcher call back method which recomputes the CellsPanelOffset
|
|
/// </summary>
|
|
private object InvalidateCellsPanelHorizontalOffset(object args)
|
|
{
|
|
if (!CellsPanelHorizontalOffsetComputationPending)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
EGC.IProvideDataGridColumn cell = GetAnyCellOrColumnHeader();
|
|
if (cell != null)
|
|
{
|
|
CellsPanelHorizontalOffset = EGC.DataGridHelper.GetParentCellsPanelHorizontalOffset(cell);
|
|
}
|
|
|
|
CellsPanelHorizontalOffsetComputationPending = false;
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which return any one of the cells or column headers
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
internal EGC.IProvideDataGridColumn GetAnyCellOrColumnHeader()
|
|
{
|
|
if (_rowTrackingRoot != null)
|
|
{
|
|
EGC.DataGridRow row = _rowTrackingRoot.Container;
|
|
EGC.DataGridCellsPresenter cellsPresenter = row.CellsPresenter;
|
|
if (cellsPresenter != null)
|
|
{
|
|
EGC.ContainerTracking<EGC.DataGridCell> cellTracker = cellsPresenter.CellTrackingRoot;
|
|
if (cellTracker != null)
|
|
{
|
|
return cellTracker.Container;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ColumnHeadersPresenter != null)
|
|
{
|
|
EGC.ContainerTracking<EGC.DataGridColumnHeader> headerTracker = ColumnHeadersPresenter.HeaderTrackingRoot;
|
|
if (headerTracker != null)
|
|
{
|
|
return headerTracker.Container;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method which returns the width of the viewport which is available for the columns to render
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
internal double GetViewportWidthForColumns()
|
|
{
|
|
if (InternalScrollHost == null)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
double totalAvailableWidth = InternalScrollHost.ViewportWidth;
|
|
totalAvailableWidth -= CellsPanelHorizontalOffset;
|
|
return totalAvailableWidth;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
// TODO: Consider making this public.
|
|
// Used as an alternate data item to CollectionView.NewItemPlaceholder so that
|
|
// CellsPresenter's ItemContainerGenerator does not get confused.
|
|
internal static object NewItemPlaceholder
|
|
{
|
|
get { return _newItemPlaceholder; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Data
|
|
|
|
private static ComponentResourceKey _focusBorderBrushKey; // Used in styles
|
|
private static IValueConverter _headersVisibilityConverter; // Used to convert DataGridHeadersVisibility to Visibility in styles
|
|
private static IValueConverter _rowDetailsScrollingConverter; // Used to convert boolean (DataGrid.RowDetailsAreFrozen) into a SelectiveScrollingMode
|
|
private static object _newItemPlaceholder = new object(); // Used as an alternate data item to CollectionView.NewItemPlaceholder
|
|
|
|
private EGC.DataGridColumnCollection _columns; // Stores the columns
|
|
private EGC.ContainerTracking<EGC.DataGridRow> _rowTrackingRoot; // Root of a linked list of active row containers
|
|
private EGC.DataGridColumnHeadersPresenter _columnHeadersPresenter; // headers presenter for sending down notifications
|
|
private EGC.DataGridCell _currentCellContainer; // Reference to the cell container corresponding to CurrentCell (use CurrentCellContainer property instead)
|
|
private EGC.DataGridCell _pendingCurrentCellContainer; // Reference to the cell container that will become the current cell
|
|
private EGC.SelectedCellsCollection _selectedCells; // Stores the selected cells
|
|
private Nullable<EGC.DataGridCellInfo> _selectionAnchor; // For doing extended selection
|
|
private bool _isDraggingSelection; // Whether a drag select is being performed
|
|
private bool _isRowDragging; // Whether a drag select is being done on rows
|
|
private Panel _internalItemsHost; // Workaround for not having access to ItemsHost
|
|
private ScrollViewer _internalScrollHost; // Scroll viewer of the datagrid
|
|
private ScrollContentPresenter _internalScrollContentPresenter = null; // Scroll Content Presenter of DataGrid's ScrollViewer
|
|
private DispatcherTimer _autoScrollTimer; // Timer to tick auto-scroll
|
|
private bool _hasAutoScrolled; // Whether an auto-scroll has occurred since starting the tick
|
|
private EGC.VirtualizedCellInfoCollection _pendingSelectedCells; // Cells that were selected that haven't gone through SelectedCellsChanged
|
|
private EGC.VirtualizedCellInfoCollection _pendingUnselectedCells; // Cells that were unselected that haven't gone through SelectedCellsChanged
|
|
private bool _deferAutoGeneration = false; // The flag which determines whether the columns generation is deferred
|
|
private bool _measureNeverInvoked = true; // Flag used to track if measure was invoked atleast once. Particularly used for AutoGeneration.
|
|
private bool _updatingSelectedCells = false; // Whether to defer notifying that SelectedCells changed.
|
|
private Visibility _placeholderVisibility = Visibility.Collapsed; // The visibility used for the Placeholder container. It may not exist at all times, so it's stored on the DG.
|
|
private Point _dragPoint; // Used to detect if a drag actually occurred
|
|
private List<int> _groupingSortDescriptionIndices = null; // List to hold the indices of SortDescriptions added for the sake of GroupDescriptions.
|
|
private bool _ignoreSortDescriptionsChange = false; // Flag used to neglect the SortDescriptionCollection changes in the CollectionChanged listener.
|
|
private bool _sortingStarted = false; // Flag used to track if Sorting ever started or not.
|
|
private ObservableCollection<ValidationRule> _rowValidationRules; // Stores the row ValidationRule's
|
|
private BindingGroup _rowValidationBindingGroup; // Cached copy of the BindingGroup created for row validation...so we dont stomp on user set ItemBindingGroup
|
|
private object _editingRowItem = null; // Current editing row item
|
|
private int _editingRowIndex = -1; // Current editing row index
|
|
private bool _hasCellValidationError; // An unsuccessful cell commit occurred
|
|
private bool _hasRowValidationError; // An unsuccessful row commit occurred
|
|
private IEnumerable _cachedItemsSource = null; // Reference to the ItemsSource instance, used to clear SortDescriptions on ItemsSource change
|
|
private EGC.DataGridItemAttachedStorage _itemAttachedStorage = new EGC.DataGridItemAttachedStorage(); // Holds data about the items that's need for row virtualization
|
|
private bool _viewportWidthChangeNotificationPending = false; // Flag to indicate if a viewport width change reuest is already queued.
|
|
private double _originalViewportWidth = 0.0; // Holds the original viewport width between multiple viewport width changes
|
|
private double _finalViewportWidth = 0.0; // Holds the final viewport width between multiple viewport width changes
|
|
|
|
#endregion
|
|
|
|
#region Constants
|
|
private const string ItemsPanelPartName = "PART_RowsPresenter";
|
|
#endregion
|
|
}
|
|
|
|
internal class NativeMethods
|
|
{
|
|
// Private constructor for performance only
|
|
private NativeMethods()
|
|
{
|
|
}
|
|
|
|
// Used for AutoScrollTimeout
|
|
[DllImport("User32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
|
|
internal static extern int GetDoubleClickTime();
|
|
}
|
|
}
|