//--------------------------------------------------------------------------- // // 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 { /// /// A DataGrid control that displays data in rows and columns and allows /// for the entering and editing of data. /// public class DataGrid : MultiSelector { #region Constructors /// /// Instantiates global information. /// 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); } /// /// Instantiates a new instance of this class. /// public DataGrid() { _columns = new EGC.DataGridColumnCollection(this); _columns.CollectionChanged += new NotifyCollectionChangedEventHandler(OnColumnsChanged); _rowValidationRules = new ObservableCollection(); _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 /// /// A collection of column definitions describing the individual /// columns of each row. /// public ObservableCollection Columns { get { return _columns; } } /// /// Returns the column collection without having to upcast from ObservableCollection /// internal EGC.DataGridColumnCollection InternalColumns { get { return _columns; } } /// /// A property that specifies whether the user can resize columns in the UI by dragging the column headers. /// /// /// This does not affect whether column widths can be changed programmatically via a property such as Column.Width. /// public bool CanUserResizeColumns { get { return (bool)GetValue(CanUserResizeColumnsProperty); } set { SetValue(CanUserResizeColumnsProperty, value); } } /// /// The DependencyProperty that represents the CanUserResizeColumns property. /// public static readonly DependencyProperty CanUserResizeColumnsProperty = DependencyProperty.Register("CanUserResizeColumns", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnNotifyColumnHeaderPropertyChanged))); /// /// Specifies the width of the header and cells within all the columns. /// public EGC.DataGridLength ColumnWidth { get { return (EGC.DataGridLength)GetValue(ColumnWidthProperty); } set { SetValue(ColumnWidthProperty, value); } } /// /// The DependencyProperty that represents the ColumnWidth property. /// public static readonly DependencyProperty ColumnWidthProperty = DependencyProperty.Register("ColumnWidth", typeof(EGC.DataGridLength), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridLength.SizeToHeader)); /// /// Specifies the minimum width of the header and cells within all columns. /// public double MinColumnWidth { get { return (double)GetValue(MinColumnWidthProperty); } set { SetValue(MinColumnWidthProperty, value); } } /// /// The DependencyProperty that represents the MinColumnWidth property. /// public static readonly DependencyProperty MinColumnWidthProperty = DependencyProperty.Register( "MinColumnWidth", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(20d, new PropertyChangedCallback(OnColumnSizeConstraintChanged)), new ValidateValueCallback(ValidateMinColumnWidth)); /// /// Specifies the maximum width of the header and cells within all columns. /// public double MaxColumnWidth { get { return (double)GetValue(MaxColumnWidthProperty); } set { SetValue(MaxColumnWidthProperty, value); } } /// /// The DependencyProperty that represents the MaxColumnWidth property. /// 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); } /// /// Validates that the minimum column width is an acceptable value /// private static bool ValidateMinColumnWidth(object v) { double value = (double)v; return !(value < 0d || DoubleUtil.IsNaN(value) || Double.IsPositiveInfinity(value)); } /// /// Validates that the maximum column width is an acceptable value /// private static bool ValidateMaxColumnWidth(object v) { double value = (double)v; return !(value < 0d || DoubleUtil.IsNaN(value)); } /// /// Called when the Columns collection changes. /// 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(); } } /// /// Updates the reference to this DataGrid on the list of columns. /// /// The list of affected columns. /// Whether to add or remove the reference to this grid. 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; } } } /// /// Updates the transferred size constraints from DataGrid on the columns. /// /// The list of affected columns. 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(); } } /// /// Helper method which determines if the /// given list has visible columns /// 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 /// /// Returns the DataGridColumn with the given DisplayIndex /// 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); } /// /// Event that is fired when the DisplayIndex on one of the DataGrid's Columns changes. /// public event EventHandler ColumnDisplayIndexChanged; /// /// Called when the DisplayIndex of a column is modified. /// /// /// 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. /// protected internal virtual void OnColumnDisplayIndexChanged(EGC.DataGridColumnEventArgs e) { if (ColumnDisplayIndexChanged != null) { ColumnDisplayIndexChanged(this, e); } } /// /// 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. /// internal List DisplayIndexMap { get { return InternalColumns.DisplayIndexMap; } } /// /// Throws an ArgumentOutOfRangeException if the given displayIndex is invalid. /// internal void ValidateDisplayIndex(EGC.DataGridColumn column, int displayIndex) { InternalColumns.ValidateDisplayIndex(column, displayIndex); } /// /// Returns the index of a column from the given DisplayIndex /// internal int ColumnIndexFromDisplayIndex(int displayIndex) { if (displayIndex >= 0 && displayIndex < DisplayIndexMap.Count) { return DisplayIndexMap[displayIndex]; } return -1; } /// /// Given the DisplayIndex of a column returns the DataGridColumnHeader for that column. /// Used by DataGridColumnHeader to find its previous sibling. /// /// /// 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 /// /// Notifies each CellsPresenter about property changes. /// private static void OnNotifyCellsPresenterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.CellsPresenter); } /// /// Notifies each Column and Cell about property changes. /// private static void OnNotifyColumnAndCellPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Columns | NotificationTarget.Cells); } /// /// Notifies each Column about property changes. /// private static void OnNotifyColumnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Columns); } /// /// Notifies the Column & Column Headers about property changes. /// private static void OnNotifyColumnAndColumnHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Columns | NotificationTarget.ColumnHeaders); } /// /// Notifies the Column Headers about property changes. /// private static void OnNotifyColumnHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnHeaders); } /// /// Notifies the Row and Column Headers about property changes (used by the AlternationBackground property) /// private static void OnNotifyHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnHeaders | NotificationTarget.RowHeaders); } /// /// Notifies the DataGrid and each Row about property changes. /// private static void OnNotifyDataGridAndRowPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Rows | NotificationTarget.DataGrid); } /// /// Notifies everyone who cares about GridLine property changes (Row, Cell, RowHeader, ColumnHeader) /// 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); } } /// /// Notifies each Row about property changes. /// private static void OnNotifyRowPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Rows); } /// /// Notifies the Row Headers about property changes. /// private static void OnNotifyRowHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.RowHeaders); } /// /// Notifies the Row & Row Headers about property changes. /// private static void OnNotifyRowAndRowHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Rows | NotificationTarget.RowHeaders); } /// /// Notifies the Row & Details about property changes. /// private static void OnNotifyRowAndDetailsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.Rows | NotificationTarget.DetailsPresenter); } /// /// Notifies HorizontalOffset change to columns collection, cellspresenter and column headers presenter /// private static void OnNotifyHorizontalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnCollection | NotificationTarget.CellsPresenter | NotificationTarget.ColumnHeadersPresenter); } /// /// General notification for DependencyProperty changes from the grid or from columns. /// /// /// 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. /// internal void NotifyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e, NotificationTarget target) { NotifyPropertyChanged(d, string.Empty, e, target); } /// /// General notification for DependencyProperty changes from the grid or from columns. /// /// /// 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. /// 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 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); } } /// /// Called by DataGridColumnCollection when columns' DisplayIndex changes /// /// internal void UpdateColumnsOnVirtualizedCellInfoCollections(NotifyCollectionChangedAction action, int oldDisplayIndex, EGC.DataGridColumn oldColumn, int newDisplayIndex) { using (UpdateSelectedCells()) { _selectedCells.OnColumnsChanged(action, oldDisplayIndex, oldColumn, newDisplayIndex, SelectedItems); } } /// /// Reference to the ColumnHeadersPresenter. The presenter sets this when it is created. /// internal EGC.DataGridColumnHeadersPresenter ColumnHeadersPresenter { get { return _columnHeadersPresenter; } set { _columnHeadersPresenter = value; } } /// /// OnTemplateChanged override /// 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; } /// /// A cell is notifying the DataGrid that its IsKeyboardFocusWithin property changed. /// internal void CellIsKeyboardFocusWithinChanged(EGC.DataGridCell cell, bool isKeyboardFocusWithin) { UpdateCurrentCell(cell, isKeyboardFocusWithin); } #endregion #region GridLines /// /// GridLinesVisibility Dependency Property /// public static readonly DependencyProperty GridLinesVisibilityProperty = DependencyProperty.Register( "GridLinesVisibility", typeof(EGC.DataGridGridLinesVisibility), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridGridLinesVisibility.All, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged))); /// /// Specifies the visibility of the DataGrid's grid lines /// public EGC.DataGridGridLinesVisibility GridLinesVisibility { get { return (EGC.DataGridGridLinesVisibility)GetValue(GridLinesVisibilityProperty); } set { SetValue(GridLinesVisibilityProperty, value); } } /// /// HorizontalGridLinesBrush Dependency Property /// public static readonly DependencyProperty HorizontalGridLinesBrushProperty = DependencyProperty.Register( "HorizontalGridLinesBrush", typeof(Brush), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(Brushes.Black, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged))); /// /// Specifies the Brush used to draw the horizontal grid lines /// public Brush HorizontalGridLinesBrush { get { return (Brush)GetValue(HorizontalGridLinesBrushProperty); } set { SetValue(HorizontalGridLinesBrushProperty, value); } } /// /// VerticalGridLinesBrush Dependency Property /// public static readonly DependencyProperty VerticalGridLinesBrushProperty = DependencyProperty.Register( "VerticalGridLinesBrush", typeof(Brush), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(Brushes.Black, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged))); /// /// Specifies the Brush used to draw the vertical grid lines /// public Brush VerticalGridLinesBrush { get { return (Brush)GetValue(VerticalGridLinesBrushProperty); } set { SetValue(VerticalGridLinesBrushProperty, value); } } #if GridLineThickness /// /// HorizontalGridLineThickness DependencyProperty /// public static readonly DependencyProperty HorizontalGridLineThicknessProperty = DependencyProperty.Register("HorizontalGridLineThickness", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(1d, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged))); /// /// Specifies the thickness of the horizontal grid lines. /// public double HorizontalGridLineThickness { get { return (double)GetValue(HorizontalGridLineThicknessProperty); } set { SetValue(HorizontalGridLineThicknessProperty, value); } } /// /// VerticalGridLineThickness DependencyProperty /// public static readonly DependencyProperty VerticalGridLineThicknessProperty = DependencyProperty.Register("VerticalGridLineThickness", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(1d, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged))); /// /// Specifies the thickness of the vertical grid lines. /// 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 /// /// Determines if an item is its own container. /// /// The item to test. /// true if the item is a DataGridRow, false otherwise. protected override bool IsItemItsOwnContainerOverride(object item) { return item is EGC.DataGridRow; } /// /// Instantiates an instance of a container. /// /// A new DataGridRow. protected override DependencyObject GetContainerForItemOverride() { return new EGC.DataGridRow(); } /// /// Prepares a new container for a given item. /// /// The new container. /// The item that the container represents. 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)); } /// /// Clears a container of references. /// /// The container being cleared. /// The data item that the container represented. 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); } /// /// Propagates the collection changed notification on Columns down to /// each active DataGridRow. /// /// The event arguments from the original collection changed event. private void UpdateColumnsOnRows(NotifyCollectionChangedEventArgs e) { EGC.ContainerTracking tracker = _rowTrackingRoot; while (tracker != null) { tracker.Container.OnColumnsChanged(_columns, e); tracker = tracker.Next; } } /// /// Equivalent of ItemContainerStyle. /// /// /// If this property has a non-null value, it will override the value /// of ItemContainerStyle. /// public Style RowStyle { get { return (Style)GetValue(RowStyleProperty); } set { SetValue(RowStyleProperty, value); } } /// /// DependencyProperty for the RowStyle property. /// 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; } /// /// Template used to visually indicate an error in row Validation. /// public ControlTemplate RowValidationErrorTemplate { get { return (ControlTemplate)GetValue(RowValidationErrorTemplateProperty); } set { SetValue(RowValidationErrorTemplateProperty, value); } } /// /// DependencyProperty for the RowValidationErrorTemplate property. /// public static readonly DependencyProperty RowValidationErrorTemplateProperty = DependencyProperty.Register("RowValidationErrorTemplate", typeof(ControlTemplate), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowPropertyChanged))); /// /// Validation rules that are run on each DataGridRow. If DataGrid.ItemBindingGroup is used, RowValidationRules is ignored. /// public ObservableCollection RowValidationRules { get { return _rowValidationRules; } } /// /// Called when the Columns collection changes. /// 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; } } } /// /// Equivalent of ItemContainerStyleSelector. /// /// /// If this property has a non-null value, it will override the value /// of ItemContainerStyleSelector. /// public StyleSelector RowStyleSelector { get { return (StyleSelector)GetValue(RowStyleSelectorProperty); } set { SetValue(RowStyleSelectorProperty, value); } } /// /// DependencyProperty for the RowStyleSelector property. /// 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; } /// /// The default row background brush. /// public Brush RowBackground { get { return (Brush)GetValue(RowBackgroundProperty); } set { SetValue(RowBackgroundProperty, value); } } /// /// DependencyProperty for RowBackground. /// public static readonly DependencyProperty RowBackgroundProperty = DependencyProperty.Register("RowBackground", typeof(Brush), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowPropertyChanged))); /// /// The default row background brush for use on every other row. /// /// /// Setting this property to a non-null value will coerce AlternationCount to 2. /// public Brush AlternatingRowBackground { get { return (Brush)GetValue(AlternatingRowBackgroundProperty); } set { SetValue(AlternatingRowBackgroundProperty, value); } } /// /// DependencyProperty for AlternatingRowBackground. /// 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; } /// /// The default height of a row. /// public double RowHeight { get { return (double)GetValue(RowHeightProperty); } set { SetValue(RowHeightProperty, value); } } /// /// The DependencyProperty for RowHeight. /// public static readonly DependencyProperty RowHeightProperty = DependencyProperty.Register("RowHeight", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(double.NaN, new PropertyChangedCallback(OnNotifyCellsPresenterPropertyChanged))); /// /// The default minimum height of a row. /// public double MinRowHeight { get { return (double)GetValue(MinRowHeightProperty); } set { SetValue(MinRowHeightProperty, value); } } /// /// The DependencyProperty for MinRowHeight. /// public static readonly DependencyProperty MinRowHeightProperty = DependencyProperty.Register("MinRowHeight", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnNotifyCellsPresenterPropertyChanged))); /// /// The NewItemPlaceholder row uses this to set its visibility while it's preparing. /// internal Visibility PlaceholderVisibility { get { return _placeholderVisibility; } } /// /// Event that is fired just before a row is prepared. /// public event EventHandler LoadingRow; /// /// Event that is fired just before a row is cleared. /// public event EventHandler UnloadingRow; /// /// Invokes the LoadingRow event /// 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; } /// /// Invokes the UnloadingRow event /// 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 /// /// The default width of a row header. /// public double RowHeaderWidth { get { return (double)GetValue(RowHeaderWidthProperty); } set { SetValue(RowHeaderWidthProperty, value); } } /// /// The DependencyProperty for RowHeaderWidth. /// public static readonly DependencyProperty RowHeaderWidthProperty = DependencyProperty.Register("RowHeaderWidth", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(double.NaN, new PropertyChangedCallback(OnNotifyRowHeaderWidthPropertyChanged))); /// /// The actual width of row headers used for binding. This is computed from the measure of all the visible row headers. /// public double RowHeaderActualWidth { get { return (double)GetValue(RowHeaderActualWidthProperty); } internal set { SetValue(RowHeaderActualWidthPropertyKey, value); } } /// /// The DependencyPropertyKey for RowHeaderActualWidth. /// private static readonly DependencyPropertyKey RowHeaderActualWidthPropertyKey = DependencyProperty.RegisterReadOnly("RowHeaderActualWidth", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnNotifyRowHeaderPropertyChanged))); /// /// The DependencyProperty for RowHeaderActualWidth. /// public static readonly DependencyProperty RowHeaderActualWidthProperty = RowHeaderActualWidthPropertyKey.DependencyProperty; /// /// The default height of a column header. /// public double ColumnHeaderHeight { get { return (double)GetValue(ColumnHeaderHeightProperty); } set { SetValue(ColumnHeaderHeightProperty, value); } } /// /// The DependencyProperty for ColumnHeaderHeight. /// public static readonly DependencyProperty ColumnHeaderHeightProperty = DependencyProperty.Register("ColumnHeaderHeight", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(double.NaN, OnNotifyColumnHeaderPropertyChanged)); /// /// A property that specifies the visibility of the column & row headers. /// public EGC.DataGridHeadersVisibility HeadersVisibility { get { return (EGC.DataGridHeadersVisibility)GetValue(HeadersVisibilityProperty); } set { SetValue(HeadersVisibilityProperty, value); } } /// /// The DependencyProperty that represents the HeadersVisibility property. /// public static readonly DependencyProperty HeadersVisibilityProperty = DependencyProperty.Register("HeadersVisibility", typeof(EGC.DataGridHeadersVisibility), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(DataGridHeadersVisibility.All)); /// /// Updates RowHeaderActualWidth to reflect changes to RowHeaderWidth /// 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); } /// /// Resets the RowHeaderActualWidth to 0.0 if in Auto mode /// private void ResetRowHeaderActualWidth() { if (DoubleUtil.IsNaN(RowHeaderWidth)) { RowHeaderActualWidth = 0.0; } } #endregion #region Item Associated Properties /// /// Sets the specified item's DetailsVisibility. /// /// /// This is useful when a DataGridRow may not currently exists to set DetailsVisibility on. /// /// The item that will have its DetailsVisibility set. /// The Visibility that the item's details should get. 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; } } /// /// Returns the current DetailsVisibility for an item that's in the DataGrid. /// /// The item who's DetailsVisibility you would like to get /// The DetailsVisibility associated with the specified item. 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; } } /// /// Clears the DetailsVisibility for the specified item /// /// The item to clear the DetailsVisibility on. 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 /// /// A style to apply to all cells in the DataGrid. /// public Style CellStyle { get { return (Style)GetValue(CellStyleProperty); } set { SetValue(CellStyleProperty, value); } } /// /// The DependencyProperty that represents the CellStyle property. /// public static readonly DependencyProperty CellStyleProperty = DependencyProperty.Register("CellStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyColumnAndCellPropertyChanged))); /// /// A style to apply to all column headers in the DataGrid /// public Style ColumnHeaderStyle { get { return (Style)GetValue(ColumnHeaderStyleProperty); } set { SetValue(ColumnHeaderStyleProperty, value); } } /// /// The DependencyProperty that represents the ColumnHeaderStyle property. /// public static readonly DependencyProperty ColumnHeaderStyleProperty = DependencyProperty.Register("ColumnHeaderStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyColumnAndColumnHeaderPropertyChanged))); /// /// A style to apply to all row headers in the DataGrid /// public Style RowHeaderStyle { get { return (Style)GetValue(RowHeaderStyleProperty); } set { SetValue(RowHeaderStyleProperty, value); } } /// /// The DependencyProperty that represents the RowHeaderStyle property. /// public static readonly DependencyProperty RowHeaderStyleProperty = DependencyProperty.Register("RowHeaderStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowAndRowHeaderPropertyChanged))); /// /// The object representing the Row Header template. /// public DataTemplate RowHeaderTemplate { get { return (DataTemplate)GetValue(RowHeaderTemplateProperty); } set { SetValue(RowHeaderTemplateProperty, value); } } /// /// The DependencyProperty for the RowHeaderTemplate property. /// public static readonly DependencyProperty RowHeaderTemplateProperty = DependencyProperty.Register("RowHeaderTemplate", typeof(DataTemplate), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowAndRowHeaderPropertyChanged))); /// /// The object representing the Row Header template selector. /// public DataTemplateSelector RowHeaderTemplateSelector { get { return (DataTemplateSelector)GetValue(RowHeaderTemplateSelectorProperty); } set { SetValue(RowHeaderTemplateSelectorProperty, value); } } /// /// The DependencyProperty for the RowHeaderTemplateSelector property. /// public static readonly DependencyProperty RowHeaderTemplateSelectorProperty = DependencyProperty.Register("RowHeaderTemplateSelector", typeof(DataTemplateSelector), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowAndRowHeaderPropertyChanged))); /// /// The default style references this brush to create a thicker border /// around the focused cell. /// public static ComponentResourceKey FocusBorderBrushKey { get { if (_focusBorderBrushKey == null) { _focusBorderBrushKey = new ComponentResourceKey(typeof(EGC.DataGrid), "FocusBorderBrushKey"); } return _focusBorderBrushKey; } } /// /// A converter which converts DataGridHeadersVisibility to VisibilityConverter based on a ConverterParameter. /// /// /// This can be used in the DataGrid's template to control which parts of the DataGrid are visible for a given DataGridHeadersVisibility. /// 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; } } /// /// A converter which converts bool to SelectiveScrollingOrientation based on a ConverterParameter. /// /// /// This can be used in the DataGrid's template to control how the RowDetails selectively scroll based on a bool. /// 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 /// /// Defines the behavior that determines the visibility of horizontal ScrollBars. /// public ScrollBarVisibility HorizontalScrollBarVisibility { get { return (ScrollBarVisibility)GetValue(HorizontalScrollBarVisibilityProperty); } set { SetValue(HorizontalScrollBarVisibilityProperty, value); } } /// /// The DependencyProperty for the HorizontalScrollBarVisibility property. /// public static readonly DependencyProperty HorizontalScrollBarVisibilityProperty = ScrollViewer.HorizontalScrollBarVisibilityProperty.AddOwner(typeof(EGC.DataGrid), new FrameworkPropertyMetadata(ScrollBarVisibility.Auto)); /// /// Defines the behavior that determines the visibility of vertical ScrollBars. /// public ScrollBarVisibility VerticalScrollBarVisibility { get { return (ScrollBarVisibility)GetValue(VerticalScrollBarVisibilityProperty); } set { SetValue(VerticalScrollBarVisibilityProperty, value); } } /// /// The DependencyProperty for the HorizontalScrollBarVisibility property. /// public static readonly DependencyProperty VerticalScrollBarVisibilityProperty = ScrollViewer.VerticalScrollBarVisibilityProperty.AddOwner(typeof(EGC.DataGrid), new FrameworkPropertyMetadata(ScrollBarVisibility.Auto)); /// /// Scrolls a row into view. /// /// The data item of the row to bring into view. 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); } } /// /// 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. /// /// The data item row that contains the cell. /// The cell's column. 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 }); } } /// /// 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. /// 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); } } /// /// Called when IsMouseCaptured changes on this element. /// 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); } } /// /// Begins a timer that will periodically scroll and select. /// 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(); } } /// /// Stops the timer that controls auto-scrolling. /// private void StopAutoScroll() { if (_autoScrollTimer != null) { _autoScrollTimer.Stop(); _autoScrollTimer = null; _hasAutoScrolled = false; } } /// /// The callback when the auto-scroll timer ticks. /// private void OnAutoScrollTimeout(object sender, EventArgs e) { if (Mouse.LeftButton == MouseButtonState.Pressed) { DoAutoScroll(); } else { StopAutoScroll(); } } /// /// Based on the mouse position relative to the rows and cells, /// scrolls and selects rows and/or cells. /// /// true if a scroll and select was attempted. false otherwise. 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; } /// /// Prevents the ScrollViewer from handling keyboard input. /// protected override bool HandlesScrolling { get { return true; } } /// /// Workaround for not having access to ItemsControl.ItemsHost. /// internal Panel InternalItemsHost { get { return _internalItemsHost; } set { if (_internalItemsHost != value) { _internalItemsHost = value; if (_internalItemsHost != null) { EnsureInternalScrollControls(); } } } } /// /// Workaround for not having access to ItemsControl.ScrollHost. /// internal ScrollViewer InternalScrollHost { get { EnsureInternalScrollControls(); return _internalScrollHost; } } /// /// Workaround for not having access to ScrollContentPresenter /// internal ScrollContentPresenter InternalScrollContentPresenter { get { EnsureInternalScrollControls(); return _internalScrollContentPresenter; } } /// /// Helper method which ensures the initialization of scroll controls. /// private void EnsureInternalScrollControls() { if (_internalScrollContentPresenter == null) { if (_internalItemsHost != null) { _internalScrollContentPresenter = EGC.DataGridHelper.FindVisualParent(_internalItemsHost); } else if (_rowTrackingRoot != null) { EGC.DataGridRow row = _rowTrackingRoot.Container; _internalScrollContentPresenter = EGC.DataGridHelper.FindVisualParent(row); } if (_internalScrollContentPresenter != null) { _internalScrollContentPresenter.SizeChanged += new SizeChangedEventHandler(OnInternalScrollContentPresenterSizeChanged); } } if (_internalScrollHost == null) { if (_internalItemsHost != null) { _internalScrollHost = EGC.DataGridHelper.FindVisualParent(_internalItemsHost); } else if (_rowTrackingRoot != null) { EGC.DataGridRow row = _rowTrackingRoot.Container; _internalScrollHost = EGC.DataGridHelper.FindVisualParent(row); } if (_internalScrollHost != null) { Binding horizontalOffsetBinding = new Binding("ContentHorizontalOffset"); horizontalOffsetBinding.Source = _internalScrollHost; SetBinding(HorizontalScrollOffsetProperty, horizontalOffsetBinding); } } } /// /// Helper method which cleans up the internal scroll controls. /// private void CleanUpInternalScrollControls() { BindingOperations.ClearBinding(this, HorizontalScrollOffsetProperty); _internalScrollHost = null; if (_internalScrollContentPresenter != null) { _internalScrollContentPresenter.SizeChanged -= new SizeChangedEventHandler(OnInternalScrollContentPresenterSizeChanged); _internalScrollContentPresenter = null; } } /// /// Size changed handler for InteralScrollContentPresenter. /// private void OnInternalScrollContentPresenterSizeChanged(object sender, SizeChangedEventArgs e) { if (_internalScrollContentPresenter != null && !_internalScrollContentPresenter.CanContentScroll) { OnViewportSizeChanged(e.PreviousSize, e.NewSize); } } /// /// Helper method which enqueues a viewport width change /// request to Dispatcher if needed. /// 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; } } } } /// /// Dispatcher callback method for Viewport width change /// which propagates the notification if needed. /// 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; } /// /// Dependency property which would be bound to ContentHorizontalOffset /// property of the ScrollViewer. /// internal static readonly DependencyProperty HorizontalScrollOffsetProperty = DependencyProperty.Register( "HorizontalScrollOffset", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(0d, OnNotifyHorizontalOffsetPropertyChanged)); /// /// The HorizontalOffset of the scroll viewer /// internal double HorizontalScrollOffset { get { return (double)GetValue(HorizontalScrollOffsetProperty); } } #endregion #region Editing Commands /// /// The command to fire and allow to route to the DataGrid in order to indicate that the /// current cell or row should begin editing. /// public static readonly RoutedCommand BeginEditCommand = new RoutedCommand(SR.Get(SRID.DataGrid_BeginEditCommandText), typeof(EGC.DataGrid)); /// /// 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. /// public static readonly RoutedCommand CommitEditCommand = new RoutedCommand(SR.Get(SRID.DataGrid_CommitEditCommandText), typeof(EGC.DataGrid)); /// /// 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. /// public static readonly RoutedCommand CancelEditCommand = new RoutedCommand(SR.Get(SRID.DataGrid_CancelEditCommandText), typeof(EGC.DataGrid)); /// /// A command that, when invoked, will delete the current row. /// 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); } /// /// Invoked to determine if the BeginEdit command can be executed. /// 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; } /// /// Invoked when the BeginEdit command is executed. /// 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 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(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)))); } /// /// Invoked to determine if the CommitEdit command can be executed. /// protected virtual void OnCanExecuteCommitEdit(CanExecuteRoutedEventArgs e) { e.CanExecute = CanEndEdit(e, /* commit = */ true); e.Handled = true; } /// /// Invoked when the CommitEdit command is executed. /// 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; } /// /// This is a helper method used to flush the dispatcher down to DataBind priority so that the bindingGroup will be ready for CommitEdit. /// /// /// 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; } /// /// Raised just before row editing is ended. /// Gives handlers the opportunity to cancel the operation. /// public event EventHandler RowEditEnding; /// /// Called just before row editing is ended. /// Gives subclasses the opportunity to cancel the operation. /// 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); } } } /// /// Raised just before cell editing is ended. /// Gives handlers the opportunity to cancel the operation. /// public event EventHandler CellEditEnding; /// /// Called just before cell editing is ended. /// Gives subclasses the opportunity to cancel the operation. /// 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); } /// /// Invoked to determine if the CancelEdit command can be executed. /// protected virtual void OnCanExecuteCancelEdit(CanExecuteRoutedEventArgs e) { e.CanExecute = CanEndEdit(e, /* commit = */ false); e.Handled = true; } /// /// Invoked when the CancelEdit command is executed. /// 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); } /// /// Invoked to determine if the Delete command can be executed. /// 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; } /// /// Invoked when the Delete command is executed. /// 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 /// /// Whether the DataGrid's rows and cells can be placed in edit mode. /// public bool IsReadOnly { get { return (bool)GetValue(IsReadOnlyProperty); } set { SetValue(IsReadOnlyProperty, value); } } /// /// The DependencyProperty for IsReadOnly. /// 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); } /// /// The object (or row) that, if not in edit mode, can be edited. /// /// /// This is the data item for the row that either has or contains focus. /// public object CurrentItem { get { return (object)GetValue(CurrentItemProperty); } set { SetValue(CurrentItemProperty, value); } } /// /// The DependencyProperty for CurrentItem. /// 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); } } /// /// The column of the CurrentItem (row) that corresponds with the current cell. /// /// /// null indicates that a cell does not have focus. The row may still have focus. /// public EGC.DataGridColumn CurrentColumn { get { return (EGC.DataGridColumn)GetValue(CurrentColumnProperty); } set { SetValue(CurrentColumnProperty, value); } } /// /// The DependencyProperty for CurrentColumn. /// 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); } } /// /// The cell that, if not in edit mode, can be edited. /// /// /// 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. /// public EGC.DataGridCellInfo CurrentCell { get { return (EGC.DataGridCellInfo)GetValue(CurrentCellProperty); } set { SetValue(CurrentCellProperty, value); } } /// /// The DependencyProperty for CurrentCell. /// 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); } /// /// An event to notify that the value of CurrentCell changed. /// public event EventHandler CurrentCellChanged; /// /// Called when the value of CurrentCell changes. /// /// Empty event arguments. 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; } } /// /// Called just before a cell will change to edit mode /// to allow handlers to prevent the cell from entering edit mode. /// public event EventHandler BeginningEdit; /// /// Called just before a cell will change to edit mode /// to all subclasses to prevent the cell from entering edit mode. /// /// /// Default implementation raises the BeginningEdit event. /// 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); } } } /// /// Called after a cell has changed to editing mode to allow /// handlers to modify the contents of the cell. /// public event EventHandler PreparingCellForEdit; /// /// Called after a cell has changed to editing mode to allow /// subclasses to modify the contents of the cell. /// /// /// Default implementation raises the PreparingCellForEdit event. /// This method is invoked from DataGridCell (instead of DataGrid) once it has entered edit mode. /// protected internal virtual void OnPreparingCellForEdit(EGC.DataGridPreparingCellForEditEventArgs e) { if (PreparingCellForEdit != null) { PreparingCellForEdit(this, e); } } /// /// Raises the BeginEdit command, which will place the current cell or row into /// edit mode. /// /// /// If the command is enabled, this will Strip to the BeginningEdit and PreparingCellForEdit /// overrides and events. /// /// true if the current cell or row enters edit mode, false otherwise. public bool BeginEdit() { return BeginEdit(/* editingEventArgs = */ null); } /// /// Raises the BeginEdit command, which will place the current cell or row into /// edit mode. /// /// /// If the command is enabled, this will Strip to the BeginningEdit and PreparingCellForEdit /// overrides and events. /// /// The event arguments, if any, that led to BeginEdit being called. May be null. /// true if the current cell or row enters edit mode, false otherwise. 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; } /// /// 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. /// /// true if the current cell or row exits edit mode, false otherwise. 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 } /// /// Raises the CancelEdit command. /// If a cell is currently in edit mode, cancels the cell edit, but leaves any row edits alone. /// /// true if the cell exits edit mode, false otherwise. internal bool CancelEdit(EGC.DataGridCell cell) { EGC.DataGridCell currentCell = CurrentCellContainer; if (currentCell != null && currentCell == cell && currentCell.IsEditing) { return CancelEdit(EGC.DataGridEditingUnit.Cell); } return true; } /// /// Raises the CancelEdit command. /// Reverts any pending editing changes to the desired editing unit and exits edit mode. /// /// Whether to cancel edit mode of the current cell or current row. /// true if the current cell or row exits edit mode, false otherwise. 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); } } /// /// 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. /// /// true if the current cell or row exits edit mode, false otherwise. 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 } /// /// Raises the CommitEdit command. /// Commits any pending changes for the given editing unit and exits edit mode. /// /// Whether to commit changes for the current cell or current row. /// Whether to exit edit mode. /// true if the current cell or row exits edit mode, false otherwise. 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 /// /// Whether the end-user can add new rows to the ItemsSource. /// public bool CanUserAddRows { get { return (bool)GetValue(CanUserAddRowsProperty); } set { SetValue(CanUserAddRowsProperty, value); } } /// /// DependencyProperty for CanUserAddRows. /// 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; } /// /// Whether the end-user can delete rows from the ItemsSource. /// public bool CanUserDeleteRows { get { return (bool)GetValue(CanUserDeleteRowsProperty); } set { SetValue(CanUserDeleteRowsProperty, value); } } /// /// DependencyProperty for CanUserDeleteRows. /// 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); } /// /// An event that is raised when a new item is created so that /// developers can initialize the item with custom default values. /// public event EGC.InitializingNewItemEventHandler InitializingNewItem; /// /// A method that is called when a new item is created so that /// overrides can initialize the item with custom default values. /// /// /// The default implementation raises the InitializingNewItem event. /// /// Event arguments that provide access to the new item. 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 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 /// /// Determines which visibility mode the Row's details use. /// public EGC.DataGridRowDetailsVisibilityMode RowDetailsVisibilityMode { get { return (EGC.DataGridRowDetailsVisibilityMode)GetValue(RowDetailsVisibilityModeProperty); } set { SetValue(RowDetailsVisibilityModeProperty, value); } } /// /// DependencyProperty for RowDetailsVisibilityMode. /// public static readonly DependencyProperty RowDetailsVisibilityModeProperty = DependencyProperty.Register("RowDetailsVisibilityMode", typeof(EGC.DataGridRowDetailsVisibilityMode), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(EGC.DataGridRowDetailsVisibilityMode.VisibleWhenSelected, OnNotifyRowAndDetailsPropertyChanged)); /// /// Controls if the row details scroll. /// public bool AreRowDetailsFrozen { get { return (bool)GetValue(AreRowDetailsFrozenProperty); } set { SetValue(AreRowDetailsFrozenProperty, value); } } /// /// DependencyProperty for AreRowDetailsFrozen. /// public static readonly DependencyProperty AreRowDetailsFrozenProperty = DependencyProperty.Register("AreRowDetailsFrozen", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(false)); /// /// Template used for the Row details. /// public DataTemplate RowDetailsTemplate { get { return (DataTemplate)GetValue(RowDetailsTemplateProperty); } set { SetValue(RowDetailsTemplateProperty, value); } } /// /// DependencyProperty for RowDetailsTemplate. /// public static readonly DependencyProperty RowDetailsTemplateProperty = DependencyProperty.Register("RowDetailsTemplate", typeof(DataTemplate), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, OnNotifyRowAndDetailsPropertyChanged)); /// /// TemplateSelector used for the Row details /// public DataTemplateSelector RowDetailsTemplateSelector { get { return (DataTemplateSelector)GetValue(RowDetailsTemplateSelectorProperty); } set { SetValue(RowDetailsTemplateSelectorProperty, value); } } /// /// DependencyProperty for RowDetailsTemplateSelector. /// public static readonly DependencyProperty RowDetailsTemplateSelectorProperty = DependencyProperty.Register("RowDetailsTemplateSelector", typeof(DataTemplateSelector), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, OnNotifyRowAndDetailsPropertyChanged)); /// /// Event that is fired just before the details of a Row is shown /// public event EventHandler LoadingRowDetails; /// /// Event that is fired just before the details of a Row is hidden /// public event EventHandler UnloadingRowDetails; /// /// Event that is fired when the visibility of a Rows details changes. /// public event EventHandler RowDetailsVisibilityChanged; /// /// Invokes the LoadingRowDetails event /// protected virtual void OnLoadingRowDetails(EGC.DataGridRowDetailsEventArgs e) { e.Row.DetailsEventStatus = EGC.DataGridRow.RowDetailsEventStatus.Loaded; if (LoadingRowDetails != null) { LoadingRowDetails(this, e); } } /// /// Invokes the UnloadingRowDetails event /// protected virtual void OnUnloadingRowDetails(EGC.DataGridRowDetailsEventArgs e) { if (UnloadingRowDetails != null) { UnloadingRowDetails(this, e); } } /// /// Invokes the RowDetailsVisibilityChanged event /// 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 /// /// A property that specifies whether the user can resize rows in the UI by dragging the row headers. /// public bool CanUserResizeRows { get { return (bool)GetValue(CanUserResizeRowsProperty); } set { SetValue(CanUserResizeRowsProperty, value); } } /// /// The DependencyProperty that represents the CanUserResizeColumns property. /// public static readonly DependencyProperty CanUserResizeRowsProperty = DependencyProperty.Register("CanUserResizeRows", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnNotifyRowHeaderPropertyChanged))); #endregion #region Selection /// /// The currently selected cells. /// public IList SelectedCells { get { return _selectedCells; } } internal EGC.SelectedCellsCollection SelectedCellsInternal { get { return _selectedCells; } } /// /// Event that fires when the SelectedCells collection changes. /// public event SelectedCellsChangedEventHandler SelectedCellsChanged; /// /// Direct notification from the SelectedCells collection of a change. /// 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(); } } } } } } /// /// Fires the public change event when there are pending cell changes. /// 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(); } } } /// /// Called when there are changes to the SelectedCells collection. /// /// Event arguments that indicate which cells were added or removed. /// /// Base implementation fires the public SelectedCellsChanged event. /// 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); } } } /// /// A command that, when invoked, will select all items in the DataGrid. /// 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); } } /// /// Selects all cells. /// 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); } } } } /// /// Unselects all cells. /// 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(); } } } /// /// Defines the selection behavior. /// /// /// The SelectionMode and the SelectionUnit properties together define /// the selection behavior for the DataGrid. /// public EGC.DataGridSelectionMode SelectionMode { get { return (EGC.DataGridSelectionMode)GetValue(SelectionModeProperty); } set { SetValue(SelectionModeProperty, value); } } /// /// The DependencyProperty for the SelectionMode property. /// 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(); } } } } /// /// Defines the selection behavior. /// /// /// The SelectionMode and the SelectionUnit properties together define /// the selection behavior for the DataGrid. /// public EGC.DataGridSelectionUnit SelectionUnit { get { return (EGC.DataGridSelectionUnit)GetValue(SelectionUnitProperty); } set { SetValue(SelectionUnitProperty, value); } } /// /// The DependencyProperty for the SelectionUnit property. /// 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); } /// /// Called when SelectedItems changes. /// 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); } /// /// Updates the IsSelected property on cells due to a change in SelectedCells. /// 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 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 rowTracker = _rowTrackingRoot; while (rowTracker != null) { EGC.DataGridRow row = rowTracker.Container; EGC.DataGridCellsPresenter cellsPresenter = row.CellsPresenter; if (cellsPresenter != null) { EGC.ContainerTracking 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); } } } } /// /// Notification that a particular cell's IsSelected property changed. /// 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); } } } /// /// There was general input that means that selection should occur on /// the given cell. /// /// The target cell. /// Whether the input also indicated that dragging should start. 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(); } } /// /// There was general input on a row header that indicated that /// selection should occur on the given row. /// /// The target row. /// Whether the input also indicated that dragging should start. 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; } /// /// 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 /// /// /// 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. /// 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(); } } } } /// /// 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 /// /// /// 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. /// 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); } /// /// Adds or Removes from SelectedItems when deferred selection is not handled by the caller. /// 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(); } } } /// /// When changing SelectedCells, do: /// using (UpdateSelectedCells()) /// { /// ... /// } /// 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; } } /// /// Handles tracking defered selection change notifications for selected cells. /// 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; } /// /// SHIFT is down or performing a drag selection. /// Multiple items can be selected. /// There is a selection anchor. /// private bool ShouldExtendSelection { get { return CanSelectMultipleItems && (_selectionAnchor != null) && (_isDraggingSelection || ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)); } } /// /// CTRL is down. /// Previous selection should not be cleared, or a selected item should be toggled. /// 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(); } /// /// Called when a keyboard key is pressed. /// 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); } } } } /// /// Called when the tab key is pressed to perform focus navigation. /// 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); } } } } /// /// Continues a drag selection. /// 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); } /// /// Ends a drag selection. /// private void OnAnyMouseUp(MouseButtonEventArgs e) { EndDragging(); } /// /// When a ContextMenu opens on a cell that isn't selected, it should /// become selected. /// 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); } } } /// /// Finds the row that contains the mouse's Y coordinate. /// /// /// Relies on InternalItemsHost. /// Meant to be used when the mouse is outside the DataGrid. /// 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; } /// /// Finds the cell that is nearest to the mouse. /// /// /// Relies on InternalItemsHost. /// 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 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; } /// /// 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. /// /// /// A value that represents the distance between the mouse and the cell. /// This is not necessarily an accurate pixel number in some cases. /// /// /// true if the cell can be a drag target. false otherwise. /// 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; } /// /// The row that the mouse is over. /// private static EGC.DataGridRow MouseOverRow { get { return EGC.DataGridHelper.FindVisualParent(Mouse.DirectlyOver as UIElement); } } // The cell that the mouse is over. private static EGC.DataGridCell MouseOverCell { get { return EGC.DataGridHelper.FindVisualParent(Mouse.DirectlyOver as UIElement); } } /// /// The mouse position relative to the ItemsHost. /// /// /// Relies on InternalItemsHost. /// 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 /// /// Dependecy property for CanUserSortColumns Property /// public static readonly DependencyProperty CanUserSortColumnsProperty = DependencyProperty.Register( "CanUserSortColumns", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true)); /// /// The property which determines whether the datagrid can be sorted by /// cells in the columns or not /// public bool CanUserSortColumns { get { return (bool)GetValue(CanUserSortColumnsProperty); } set { SetValue(CanUserSortColumnsProperty, value); } } public event DataGridSortingEventHandler Sorting; /// /// Protected method which raises the sorting event and does default sort /// /// 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); } } /// /// Method to perform sorting on datagrid /// /// 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); } } } } /// /// Clears the sort directions for all the columns except the column to be sorted upon /// /// 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; } } } } /// /// Determines the sort direction and sort property name and adds a sort /// description to the Items>SortDescriptions Collection. Clears all the /// existing sort descriptions. /// /// /// private void DefaultSort(EGC.DataGridColumn column, bool clearExistingSortDescriptions) { ListSortDirection sortDirection = ListSortDirection.Ascending; Nullable 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; } } /// /// List which holds all the indices of SortDescriptions which were /// added for the sake of GroupDescriptions /// private List GroupingSortDescriptionIndices { get { return _groupingSortDescriptionIndices; } set { _groupingSortDescriptionIndices = value; } } /// /// SortDescription collection changed listener. Ensures that GroupingSortDescriptionIndices /// is in sync with SortDescriptions. /// /// /// 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; } } /// /// Method to remove all the SortDescriptions which were added based on GroupDescriptions /// 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; } } /// /// Helper method which determines if one can create a SortDescription out of /// a GroupDescription. /// /// /// private static bool CanConvertToSortDescription(PropertyGroupDescription propertyGroupDescription) { if (propertyGroupDescription != null && propertyGroupDescription.Converter == null && propertyGroupDescription.StringComparison == StringComparison.Ordinal) { return true; } return false; } /// /// Method to add SortDescriptions based on GroupDescriptions. /// Only PropertGroupDescriptions with no ValueConverter and with /// Oridinal comparison are considered suitable. /// 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(); } GroupingSortDescriptionIndices.Add(insertIndex++); } } } finally { _ignoreSortDescriptionsChange = originalIgnoreSortDescriptionChanges; } } /// /// Method to regenrated the SortDescriptions based on the GroupDescriptions /// private void RegenerateGroupingSortDescriptions() { RemoveGroupingSortDescriptions(); AddGroupingSortDescriptions(); } /// /// CollectionChanged listener for GroupDescriptions of DataGrid. /// Regenerates Grouping based sort descriptions is required. /// /// /// 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 /// /// This event will be raised whenever auto generation of columns gets completed /// public event EventHandler AutoGeneratedColumns; /// /// This event will be raised for each column getting auto generated /// public event EventHandler AutoGeneratingColumn; /// /// The DependencyProperty that represents the AutoGenerateColumns property. /// public static readonly DependencyProperty AutoGenerateColumnsProperty = DependencyProperty.Register("AutoGenerateColumns", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnAutoGenerateColumnsPropertyChanged))); /// /// The property which determines whether the columns are to be auto generated or not. /// Setting of the property actually generates or deletes columns. /// public bool AutoGenerateColumns { get { return (bool)GetValue(AutoGenerateColumnsProperty); } set { SetValue(AutoGenerateColumnsProperty, value); } } /// /// The polumorphic method which raises the AutoGeneratedColumns event /// /// protected virtual void OnAutoGeneratedColumns(EventArgs e) { if (AutoGeneratedColumns != null) { AutoGeneratedColumns(this, e); } } /// /// The polymorphic method which raises the AutoGeneratingColumn event /// /// protected virtual void OnAutoGeneratingColumn(EGC.DataGridAutoGeneratingColumnEventArgs e) { if (AutoGeneratingColumn != null) { AutoGeneratingColumn(this, e); } } /// /// Determines the desired size of the control given a constraint. /// /// /// On the first measure: /// - Performs auto-generation of columns if needed. /// - Coerces CanUserAddRows and CanUserDeleteRows. /// - Updates the NewItemPlaceholder. /// /// The available space. /// The desired size of the control. 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); } /// /// Coercion callback for ItemsSource property /// /// /// 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. /// 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; } /// /// The polymorphic method which gets called whenever the ItemsSource gets changed. /// We regenerate columns if required when ItemsSource gets changed. /// /// /// 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; } /// /// Private property to hook and unhook collection changed event on ItemsSources CollectionChanged /// event when ever _deferAutoGeneration flag changes. /// 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); } } } } } /// /// The event listener to ItemsSource collection changed event which performs deffered auto generation /// and also unhooks it self as needed. /// /// /// 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; } } /// /// Method which generated auto columns and adds to the data grid. /// 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); } } /// /// Method which deletes all the auto generated columns. /// 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; } } /// /// Method which regenerates the columns for the datagrid /// private void RegenerateAutoColumns() { DeleteAutoColumns(); AddAutoColumns(); } /// /// Helper method which generates columns for a given IItemProperties /// /// /// public static Collection GenerateColumns(IItemProperties itemProperties) { if (itemProperties == null) { throw new ArgumentNullException("itemProperties"); } Collection columnCollection = new Collection(); EGC.DataGrid.GenerateColumns( itemProperties, null, columnCollection); return columnCollection; } /// /// 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. /// /// /// /// private static void GenerateColumns( IItemProperties iItemProperties, EGC.DataGrid dataGrid, Collection 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 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); } } } } /// /// The event listener which listens to the change in the AutoGenerateColumns flag /// /// /// 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 /// /// Dependency Property fro FrozenColumnCount Property /// 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)); /// /// Property which determines the number of columns which are frozen from the beginning in order of display /// public int FrozenColumnCount { get { return (int)GetValue(FrozenColumnCountProperty); } set { SetValue(FrozenColumnCountProperty, value); } } /// /// Coercion call back for FrozenColumnCount property, which ensures that it is never more that column count /// /// /// /// 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; } /// /// Property changed callback fro FrozenColumnCount /// /// /// private static void OnFrozenColumnCountPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.ColumnCollection | NotificationTarget.ColumnHeadersPresenter | NotificationTarget.CellsPresenter); } /// /// Validation call back for frozen column count /// /// /// private static bool ValidateFrozenColumnCount(object value) { int frozenCount = (int)value; return frozenCount >= 0; } /// /// Dependency Property key for NonFrozenColumnsViewportHorizontalOffset Property /// private static readonly DependencyPropertyKey NonFrozenColumnsViewportHorizontalOffsetPropertyKey = DependencyProperty.RegisterReadOnly( "NonFrozenColumnsViewportHorizontalOffset", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(0.0)); /// /// Dependency property for NonFrozenColumnsViewportHorizontalOffset Property /// public static readonly DependencyProperty NonFrozenColumnsViewportHorizontalOffsetProperty = NonFrozenColumnsViewportHorizontalOffsetPropertyKey.DependencyProperty; /// /// Property which gets/sets the start x coordinate of non frozen columns in view port /// public double NonFrozenColumnsViewportHorizontalOffset { get { return (double)GetValue(NonFrozenColumnsViewportHorizontalOffsetProperty); } internal set { SetValue(NonFrozenColumnsViewportHorizontalOffsetPropertyKey, value); } } /// /// Override of OnApplyTemplate which clear the scroll host member /// public override void OnApplyTemplate() { CleanUpInternalScrollControls(); base.OnApplyTemplate(); } #endregion #region Container Virtualization /// /// Property which determines if row virtualization is enabled or disabled /// public bool EnableRowVirtualization { get { return (bool)GetValue(EnableRowVirtualizationProperty); } set { SetValue(EnableRowVirtualizationProperty, value); } } /// /// Dependency property for EnableRowVirtualization /// public static readonly DependencyProperty EnableRowVirtualizationProperty = DependencyProperty.Register( "EnableRowVirtualization", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnEnableRowVirtualizationChanged))); /// /// Property changed callback for EnableRowVirtualization. /// Keeps VirtualizingStackPanel.IsVirtualizingProperty in sync. /// 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(); } } /// /// Coercion callback for VirtualizingStackPanel.IsVirtualizingProperty /// private static object OnCoerceIsVirtualizingProperty(DependencyObject d, object baseValue) { if (!EGC.DataGridHelper.IsDefaultValue(d, EGC.DataGrid.EnableRowVirtualizationProperty)) { return d.GetValue(EGC.DataGrid.EnableRowVirtualizationProperty); } return baseValue; } /// /// Property which determines if column virtualization is enabled or disabled /// public bool EnableColumnVirtualization { get { return (bool)GetValue(EnableColumnVirtualizationProperty); } set { SetValue(EnableColumnVirtualizationProperty, value); } } /// /// Dependency property for EnableColumnVirtualization /// public static readonly DependencyProperty EnableColumnVirtualizationProperty = DependencyProperty.Register( "EnableColumnVirtualization", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnEnableColumnVirtualizationChanged))); /// /// Property changed callback for EnableColumnVirtualization. /// Gets VirtualizingStackPanel.IsVirtualizingProperty for cells presenter and /// headers presenter in sync. /// private static void OnEnableColumnVirtualizationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((EGC.DataGrid)d).NotifyPropertyChanged(d, e, NotificationTarget.CellsPresenter | NotificationTarget.ColumnHeadersPresenter | NotificationTarget.ColumnCollection); } #endregion #region Column Reordering /// /// Dependency Property for CanUserReorderColumns Property /// public static readonly DependencyProperty CanUserReorderColumnsProperty = DependencyProperty.Register("CanUserReorderColumns", typeof(bool), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(true)); /// /// The property which determines if an end user can re-order columns or not. /// public bool CanUserReorderColumns { get { return (bool)GetValue(CanUserReorderColumnsProperty); } set { SetValue(CanUserReorderColumnsProperty, value); } } /// /// Dependency Property for DragIndicatorStyle property /// public static readonly DependencyProperty DragIndicatorStyleProperty = DependencyProperty.Register("DragIndicatorStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null, OnNotifyColumnPropertyChanged)); /// /// The style property which would be applied on the column header drag indicator /// public Style DragIndicatorStyle { get { return (Style)GetValue(DragIndicatorStyleProperty); } set { SetValue(DragIndicatorStyleProperty, value); } } /// /// Dependency Property for DropLocationIndicatorStyle property /// public static readonly DependencyProperty DropLocationIndicatorStyleProperty = DependencyProperty.Register("DropLocationIndicatorStyle", typeof(Style), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(null)); /// /// The style property which would be applied on the column header drop location indicator. /// public Style DropLocationIndicatorStyle { get { return (Style)GetValue(DropLocationIndicatorStyleProperty); } set { SetValue(DropLocationIndicatorStyleProperty, value); } } public event EventHandler ColumnReordering; public event EventHandler ColumnHeaderDragStarted; public event EventHandler ColumnHeaderDragDelta; public event EventHandler ColumnHeaderDragCompleted; public event EventHandler 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 /// /// The DependencyProperty that represents the ClipboardCopyMode property. /// 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(); } /// /// The property which determines how DataGrid content is copied to the Clipboard. /// 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); } /// /// This virtual method is called when ApplicationCommands.Copy command query its state. /// /// 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); } /// /// This virtual method is called when ApplicationCommands.Copy command is executed. /// /// 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 formats = new Collection(new string[] { DataFormats.Html, DataFormats.Text, DataFormats.UnicodeText, DataFormats.CommaSeparatedValue }); Dictionary dataGridStringBuilders = new Dictionary(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(); } } /// /// 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 /// /// Contains the necessary information for generating the row clipboard content. 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); } } /// /// This event is raised by OnCopyingRowClipboardContent method after the default row content is prepared. /// Event listeners can modify or add to the row clipboard content /// public event EventHandler CopyingRowClipboardContent; #endregion #region Cells Panel Width /// /// Dependency Property for CellsPanelActualWidth property /// internal static readonly DependencyProperty CellsPanelActualWidthProperty = DependencyProperty.Register( "CellsPanelActualWidth", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(CellsPanelActualWidthChanged))); /// /// The property which represents the actual width of the cells panel, /// to be used by headers presenter /// internal double CellsPanelActualWidth { get { return (double)GetValue(CellsPanelActualWidthProperty); } set { SetValue(CellsPanelActualWidthProperty, value); } } /// /// Property changed callback for CellsPanelActualWidth property /// /// /// 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 /// /// Dependency Property Key for CellsPanelHorizontalOffset property /// private static readonly DependencyPropertyKey CellsPanelHorizontalOffsetPropertyKey = DependencyProperty.RegisterReadOnly( "CellsPanelHorizontalOffset", typeof(double), typeof(EGC.DataGrid), new FrameworkPropertyMetadata(0d, OnNotifyHorizontalOffsetPropertyChanged)); /// /// Dependency Property for CellsPanelHorizontalOffset /// public static readonly DependencyProperty CellsPanelHorizontalOffsetProperty = CellsPanelHorizontalOffsetPropertyKey.DependencyProperty; /// /// Property which caches the cells panel horizontal offset /// public double CellsPanelHorizontalOffset { get { return (double)GetValue(CellsPanelHorizontalOffsetProperty); } private set { SetValue(CellsPanelHorizontalOffsetPropertyKey, value); } } /// /// Property which indicates whether a request to /// invalidate CellsPanelOffset is already in queue or not. /// private bool CellsPanelHorizontalOffsetComputationPending { get; set; } /// /// Helper method which queue a request to dispatcher to /// invalidate the cellspanel offset if not already queued /// internal void QueueInvalidateCellsPanelHorizontalOffset() { if (!CellsPanelHorizontalOffsetComputationPending) { Dispatcher.BeginInvoke(new DispatcherOperationCallback(InvalidateCellsPanelHorizontalOffset), DispatcherPriority.Loaded, this); CellsPanelHorizontalOffsetComputationPending = true; } } /// /// Dispatcher call back method which recomputes the CellsPanelOffset /// 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; } /// /// Helper method which return any one of the cells or column headers /// /// internal EGC.IProvideDataGridColumn GetAnyCellOrColumnHeader() { if (_rowTrackingRoot != null) { EGC.DataGridRow row = _rowTrackingRoot.Container; EGC.DataGridCellsPresenter cellsPresenter = row.CellsPresenter; if (cellsPresenter != null) { EGC.ContainerTracking cellTracker = cellsPresenter.CellTrackingRoot; if (cellTracker != null) { return cellTracker.Container; } } } if (ColumnHeadersPresenter != null) { EGC.ContainerTracking headerTracker = ColumnHeadersPresenter.HeaderTrackingRoot; if (headerTracker != null) { return headerTracker.Container; } } return null; } /// /// Helper method which returns the width of the viewport which is available for the columns to render /// /// 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 _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 _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 _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 _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(); } }