Code:
/ 4.0 / 4.0 / untmp / DEVDIV_TFS / Dev10 / Releases / RTMRel / wpf / src / Framework / System / Windows / Controls / DataGrid.cs / 1407647 / DataGrid.cs
//---------------------------------------------------------------------------- // // 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.Security.Permissions; using System.Text; using System.Windows; using System.Windows.Automation.Peers; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using MS.Internal; using System.Windows.Automation; namespace System.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(DataGrid); DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(typeof(DataGrid))); FrameworkElementFactory dataGridRowPresenterFactory = new FrameworkElementFactory(typeof(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))); IsKeyboardFocusWithinPropertyKey.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(new PropertyChangedCallback(OnIsKeyboardFocusWithinChanged))); 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.RegisterClassCommandBinding(ownerType, new CommandBinding(SelectAllCommand, new ExecutedRoutedEventHandler(OnExecutedSelectAll), new CanExecuteRoutedEventHandler(OnCanExecuteSelectAll))); CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(DeleteCommand, new ExecutedRoutedEventHandler(OnExecutedDelete), new CanExecuteRoutedEventHandler(OnCanExecuteDelete))); // Default Clipboard handling CommandManager.RegisterClassCommandBinding(typeof(DataGrid), new CommandBinding(ApplicationCommands.Copy, new ExecutedRoutedEventHandler(OnExecutedCopy), new CanExecuteRoutedEventHandler(OnCanExecuteCopy))); EventManager.RegisterClassHandler(typeof(DataGrid), MouseUpEvent, new MouseButtonEventHandler(OnAnyMouseUpThunk), true); } ////// Instantiates a new instance of this class. /// public DataGrid() { _columns = new DataGridColumnCollection(this); _columns.CollectionChanged += new NotifyCollectionChangedEventHandler(OnColumnsChanged); _rowValidationRules = new ObservableCollection(); _rowValidationRules.CollectionChanged += new NotifyCollectionChangedEventHandler(OnRowValidationRulesChanged); _selectedCells = new 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 ObservableCollectionColumns { get { return _columns; } } /// /// Returns the column collection without having to upcast from ObservableCollection /// internal 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(DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnNotifyColumnAndColumnHeaderPropertyChanged))); ////// Specifies the width of the header and cells within all the columns. /// public DataGridLength ColumnWidth { get { return (DataGridLength)GetValue(ColumnWidthProperty); } set { SetValue(ColumnWidthProperty, value); } } ////// The DependencyProperty that represents the ColumnWidth property. /// public static readonly DependencyProperty ColumnWidthProperty = DependencyProperty.Register("ColumnWidth", typeof(DataGridLength), typeof(DataGrid), new FrameworkPropertyMetadata(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(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(DataGrid), new FrameworkPropertyMetadata(double.PositiveInfinity, new PropertyChangedCallback(OnColumnSizeConstraintChanged)), new ValidateValueCallback(ValidateMaxColumnWidth)); private static void OnColumnSizeConstraintChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.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++) { DataGridColumn column = (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 = (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 (DataGridColumn column in columns) { if (column.IsVisible) { return true; } } } return false; } #endregion #region Display Index ////// Returns the DataGridColumn with the given DisplayIndex /// public 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 EventHandlerColumnDisplayIndexChanged; /// /// 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(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 ListDisplayIndexMap { get { return InternalColumns.DisplayIndexMap; } } /// /// Throws an ArgumentOutOfRangeException if the given displayIndex is invalid. /// internal void ValidateDisplayIndex(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 DataGridColumnHeader ColumnHeaderFromDisplayIndex(int displayIndex) { int columnIndex = ColumnIndexFromDisplayIndex(displayIndex); if (columnIndex != -1) { if (ColumnHeadersPresenter != null && ColumnHeadersPresenter.ItemContainerGenerator != null) { return (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) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.CellsPresenter); } ////// Notifies each Column and Cell about property changes. /// private static void OnNotifyColumnAndCellPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.Columns | DataGridNotificationTarget.Cells); } ////// Notifies each Column about property changes. /// private static void OnNotifyColumnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.Columns); } ////// Notifies the Column & Column Headers about property changes. /// private static void OnNotifyColumnAndColumnHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.Columns | DataGridNotificationTarget.ColumnHeaders); } ////// Notifies the Column Headers about property changes. /// private static void OnNotifyColumnHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.ColumnHeaders); } ////// Notifies the Row and Column Headers about property changes (used by the AlternationBackground property) /// private static void OnNotifyHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.ColumnHeaders | DataGridNotificationTarget.RowHeaders); } ////// Notifies the DataGrid and each Row about property changes. /// private static void OnNotifyDataGridAndRowPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.Rows | DataGridNotificationTarget.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) { ((DataGrid)d).OnItemTemplateChanged(null, null); } } ////// Notifies each Row about property changes. /// private static void OnNotifyRowPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.Rows); } ////// Notifies the Row Headers about property changes. /// private static void OnNotifyRowHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.RowHeaders); } ////// Notifies the Row & Row Headers about property changes. /// private static void OnNotifyRowAndRowHeaderPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.Rows | DataGridNotificationTarget.RowHeaders); } ////// Notifies the Row & Details about property changes. /// private static void OnNotifyRowAndDetailsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.Rows | DataGridNotificationTarget.DetailsPresenter); } ////// Notifies HorizontalOffset change to columns collection, cellspresenter and column headers presenter /// private static void OnNotifyHorizontalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.ColumnCollection | DataGridNotificationTarget.CellsPresenter | DataGridNotificationTarget.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, DataGridNotificationTarget 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, DataGridNotificationTarget target) { if (DataGridHelper.ShouldNotifyDataGrid(target)) { if (e.Property == AlternatingRowBackgroundProperty) { // If the alternate row background is set, the count may be coerced to 2 CoerceValue(AlternationCountProperty); } else if ((e.Property == DataGridColumn.VisibilityProperty) || (e.Property == DataGridColumn.WidthProperty) || (e.Property == DataGridColumn.DisplayIndexProperty)) { // DataGridCellsPanel needs to be re-measured when column visibility changes // Recyclable containers may not be fully remeasured when they are brought in foreach (DependencyObject container in ItemContainerGenerator.RecyclableContainers) { DataGridRow row = container as DataGridRow; if (row != null) { var cellsPresenter = row.CellsPresenter; if (cellsPresenter != null) { cellsPresenter.InvalidateDataGridCellsPanelMeasureAndArrange(); } } } } } // Rows, Cells, CellsPresenter, DetailsPresenter or RowHeaders if (DataGridHelper.ShouldNotifyRowSubtree(target)) { // Notify the Rows about the property change ContainerTrackingtracker = _rowTrackingRoot; while (tracker != null) { tracker.Container.NotifyPropertyChanged(d, propertyName, e, target); tracker = tracker.Next; } } if (DataGridHelper.ShouldNotifyColumnCollection(target) || DataGridHelper.ShouldNotifyColumns(target)) { InternalColumns.NotifyPropertyChanged(d, propertyName, e, target); } if ((DataGridHelper.ShouldNotifyColumnHeadersPresenter(target) || 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, 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 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; } #endregion #region GridLines ////// GridLinesVisibility Dependency Property /// public static readonly DependencyProperty GridLinesVisibilityProperty = DependencyProperty.Register( "GridLinesVisibility", typeof(DataGridGridLinesVisibility), typeof(DataGrid), new FrameworkPropertyMetadata(DataGridGridLinesVisibility.All, new PropertyChangedCallback(OnNotifyGridLinePropertyChanged))); ////// Specifies the visibility of the DataGrid's grid lines /// public DataGridGridLinesVisibility GridLinesVisibility { get { return (DataGridGridLinesVisibility)GetValue(GridLinesVisibilityProperty); } set { SetValue(GridLinesVisibilityProperty, value); } } ////// HorizontalGridLinesBrush Dependency Property /// public static readonly DependencyProperty HorizontalGridLinesBrushProperty = DependencyProperty.Register( "HorizontalGridLinesBrush", typeof(Brush), typeof(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(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(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(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 DataGridRow; } ////// Instantiates an instance of a container. /// ///A new DataGridRow. protected override DependencyObject GetContainerForItemOverride() { return new 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); DataGridRow row = (DataGridRow)element; if (row.DataGridOwner != this) { row.Tracker.StartTracking(ref _rowTrackingRoot); EnsureInternalScrollControls(); } row.PrepareRow(item, this); OnLoadingRow(new 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); DataGridRow row = (DataGridRow)element; if (row.DataGridOwner == this) { row.Tracker.StopTracking(ref _rowTrackingRoot); } OnUnloadingRow(new 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) { ContainerTrackingtracker = _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(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 (!DataGridHelper.IsDefaultValue(d, DataGrid.RowStyleProperty)) { return d.GetValue(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(DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnNotifyRowPropertyChanged))); ////// Validation rules that are run on each DataGridRow. If DataGrid.ItemBindingGroup is used, RowValidationRules is ignored. /// public ObservableCollectionRowValidationRules { get { return _rowValidationRules; } } /// /// Called when the Columns collection changes. /// private void OnRowValidationRulesChanged(object sender, NotifyCollectionChangedEventArgs e) { EnsureItemBindingGroup(); // only update the ItemBindingGroup if it's not user created. if (_defaultBindingGroup != null) { if (object.ReferenceEquals(ItemBindingGroup, _defaultBindingGroup)) { switch (e.Action) { case NotifyCollectionChangedAction.Add: foreach (ValidationRule rule in e.NewItems) { _defaultBindingGroup.ValidationRules.Add(rule); } break; case NotifyCollectionChangedAction.Remove: foreach (ValidationRule rule in e.OldItems) { _defaultBindingGroup.ValidationRules.Remove(rule); } break; case NotifyCollectionChangedAction.Replace: foreach (ValidationRule rule in e.OldItems) { _defaultBindingGroup.ValidationRules.Remove(rule); } foreach (ValidationRule rule in e.NewItems) { _defaultBindingGroup.ValidationRules.Add(rule); } break; case NotifyCollectionChangedAction.Reset: _defaultBindingGroup.ValidationRules.Clear(); break; } } else { _defaultBindingGroup = 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(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 (!DataGridHelper.IsDefaultValue(d, DataGrid.RowStyleSelectorProperty)) { return d.GetValue(DataGrid.RowStyleSelectorProperty); } return baseValue; } private static object OnCoerceIsSynchronizedWithCurrentItem(DependencyObject d, object baseValue) { DataGrid dataGrid = (DataGrid)d; if (dataGrid.SelectionUnit == 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(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(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) { DataGrid dataGrid = (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(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(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 EventHandlerLoadingRow; /// /// Event that is fired just before a row is cleared. /// public event EventHandlerUnloadingRow; /// /// Invokes the LoadingRow event /// protected virtual void OnLoadingRow(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). Dispatcher.CurrentDispatcher.BeginInvoke(new DispatcherOperationCallback(DelayedOnLoadingRowDetails), DispatcherPriority.Loaded, row); } } internal static object DelayedOnLoadingRowDetails(object arg) { var row = (DataGridRow)arg; var dataGrid = row.DataGridOwner; if (dataGrid != null) { dataGrid.OnLoadingRowDetailsWrapper(row); } return null; } ////// Invokes the UnloadingRow event /// protected virtual void OnUnloadingRow(DataGridRowEventArgs e) { if (UnloadingRow != null) { UnloadingRow(this, e); } var row = e.Row; OnUnloadingRowDetailsWrapper(row); } #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(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(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(DataGrid), new FrameworkPropertyMetadata(double.NaN, OnNotifyColumnHeaderPropertyChanged)); ////// A property that specifies the visibility of the column & row headers. /// public DataGridHeadersVisibility HeadersVisibility { get { return (DataGridHeadersVisibility)GetValue(HeadersVisibilityProperty); } set { SetValue(HeadersVisibilityProperty, value); } } ////// The DependencyProperty that represents the HeadersVisibility property. /// public static readonly DependencyProperty HeadersVisibilityProperty = DependencyProperty.Register("HeadersVisibility", typeof(DataGridHeadersVisibility), typeof(DataGrid), new FrameworkPropertyMetadata(DataGridHeadersVisibility.All)); ////// Updates RowHeaderActualWidth to reflect changes to RowHeaderWidth /// private static void OnNotifyRowHeaderWidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var dataGrid = ((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, DataGridRow.DetailsVisibilityProperty, detailsVisibility); var row = (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, DataGridRow.DetailsVisibilityProperty, out detailsVisibility)) { return (Visibility)detailsVisibility; } var row = (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 DataGridRowDetailsVisibilityMode.VisibleWhenSelected: return SelectedItems.Contains(item) ? Visibility.Visible : Visibility.Collapsed; case 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, DataGridRow.DetailsVisibilityProperty); var row = (DataGridRow)ItemContainerGenerator.ContainerFromItem(item); if (row != null) { row.ClearValue(DataGridRow.DetailsVisibilityProperty); } } internal DataGridItemAttachedStorage ItemAttachedStorage { get { return _itemAttachedStorage; } } ////// Determines whether the selection change caused by keyboard input should select a full row (or full rows). /// private bool ShouldSelectRowHeader { get { return _selectionAnchor != null && SelectedItems.Contains(_selectionAnchor.Value.Item) && SelectionUnit == DataGridSelectionUnit.CellOrRowHeader && (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift; } } #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(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(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(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(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(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 { return SystemResourceKey.DataGridFocusBorderBrushKey; } } ////// 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 DataGridHeadersVisibilityToVisibilityConverter(); } return _headersVisibilityConverter; } } ////// A converter which converts bool to SelectiveScrollin----entation 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 BooleanToSelectiveScrollin----entationConverter(); } 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(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(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, 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], (DataGridColumn)arguments[1]); } else { ScrollColumnIntoView((DataGridColumn)arguments[1]); } } else { ScrollRowIntoView(arg); } return null; } private void ScrollColumnIntoView(DataGridColumn column) { if (_rowTrackingRoot != null) { DataGridRow row = _rowTrackingRoot.Container; if (row != null) { int columnIndex = _columns.IndexOf(column); row.ScrollCellIntoView(columnIndex); } } } // 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. DataGridRowsPresenter itemsHost = InternalItemsHost as 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); } } } } // private void ScrollCellIntoView(object item, 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 DataGridRow row = ItemContainerGenerator.ContainerFromItem(item) as DataGridRow; if (row == null) { ScrollRowIntoView(item); UpdateLayout(); row = ItemContainerGenerator.ContainerFromItem(item) as DataGridRow; } else { // Ensures that row is brought into viewport row.BringIntoView(); } // 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); } ////// 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 new 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. DataGridCell cell = GetCellNearMouse(); if (cell != null) { 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); DataGridRow row = (DataGridRow)ItemContainerGenerator.ContainerFromItem(dataItem); if (row != null) { _hasAutoScrolled = true; HandleSelectionForRowHeaderAndDetailsInput(row, /* startDragging = */ false); SetCurrentValueInternal(CurrentItemProperty, 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 internal 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 = DataGridHelper.FindVisualParent(_internalItemsHost); } else if (_rowTrackingRoot != null) { DataGridRow row = _rowTrackingRoot.Container; _internalScrollContentPresenter = DataGridHelper.FindVisualParent (row); } if (_internalScrollContentPresenter != null) { _internalScrollContentPresenter.SizeChanged += new SizeChangedEventHandler(OnInternalScrollContentPresenterSizeChanged); } } if (_internalScrollHost == null) { if (_internalItemsHost != null) { _internalScrollHost = DataGridHelper.FindVisualParent (_internalItemsHost); } else if (_rowTrackingRoot != null) { DataGridRow row = _rowTrackingRoot.Container; _internalScrollHost = 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(), DataGridNotificationTarget.CellsPresenter | DataGridNotificationTarget.ColumnHeadersPresenter | DataGridNotificationTarget.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(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("BeginEdit", typeof(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("CommitEdit", typeof(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("CancelEdit", typeof(DataGrid)); ////// A command that, when invoked, will delete the current row. /// public static RoutedUICommand DeleteCommand { get { return ApplicationCommands.Delete; } } private static void OnCanExecuteBeginEdit(object sender, CanExecuteRoutedEventArgs e) { ((DataGrid)sender).OnCanExecuteBeginEdit(e); } private static void OnExecutedBeginEdit(object sender, ExecutedRoutedEventArgs e) { ((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) { 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; } } if (canExecute) { e.CanExecute = true; e.Handled = true; } else { e.ContinueRouting = true; } } ////// Invoked when the BeginEdit command is executed. /// protected virtual void OnExecutedBeginEdit(ExecutedRoutedEventArgs e) { DataGridCell cell = CurrentCellContainer; if ((cell != null) && !cell.IsReadOnly && !cell.IsEditing) { bool addedPlaceholder = false; bool deselectedPlaceholder = false; bool reselectPlaceholderCells = false; ListcolumnIndexRanges = 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(); SetCurrentValueInternal(CurrentItemProperty, 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; DataGridBeginningEditEventArgs beginningEditEventArgs = null; if (cell != null) { // Give the callback an opportunity to cancel edit mode beginningEditEventArgs = new 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; // Create a CellAutomationValueHolder object that has a binding to the content of the CurrentCell being edited. // This is required to raise PropertyChanged AutomationEvent when cell is being edited manually. EnsureCellAutomationValueHolder(cell); } } // CancelEdit and CommitEdit rely on IsAddingNewItem and IsEditingRowItem CommandManager.InvalidateRequerySuggested(); e.Handled = true; } private static void OnCanExecuteCommitEdit(object sender, CanExecuteRoutedEventArgs e) { ((DataGrid)sender).OnCanExecuteCommitEdit(e); } private static void OnExecutedCommitEdit(object sender, ExecutedRoutedEventArgs e) { ((DataGrid)sender).OnExecutedCommitEdit(e); } private 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 : DataGridHelper.FindVisualParent (source); } private bool CanEndEdit(CanExecuteRoutedEventArgs e, bool commit) { DataGridCell cellContainer = GetEventCellOrCurrentCell(e); if (cellContainer == null) { // If there is no cell, then nothing can be determined. So, no edit could end. return false; } 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 || (!HasCellValidationError && IsAddingOrEditingRowItem(editingUnit, rowItem)); } /// /// Invoked to determine if the CommitEdit command can be executed. /// protected virtual void OnCanExecuteCommitEdit(CanExecuteRoutedEventArgs e) { if (CanEndEdit(e, /* commit = */ true)) { e.CanExecute = true; e.Handled = true; } else { e.ContinueRouting = true; } } ////// Invoked when the CommitEdit command is executed. /// protected virtual void OnExecutedCommitEdit(ExecutedRoutedEventArgs e) { DataGridCell cell = CurrentCellContainer; bool validationPassed = true; if (cell != null) { DataGridEditingUnit editingUnit = GetEditingUnit(e.Parameter); bool eventCanceled = false; if (cell.IsEditing) { DataGridCellEditEndingEventArgs cellEditEndingEventArgs = new DataGridCellEditEndingEventArgs(cell.Column, cell.RowOwner, cell.EditingElement, DataGridEditAction.Commit); OnCellEditEnding(cellEditEndingEventArgs); eventCanceled = cellEditEndingEventArgs.Cancel; if (!eventCanceled) { validationPassed = cell.CommitEdit(); HasCellValidationError = !validationPassed; UpdateCellAutomationValueHolder(cell); } } // 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 is being edited or added and being targeted directly. if (validationPassed && !eventCanceled && IsAddingOrEditingRowItem(editingUnit, cell.RowDataItem)) { DataGridRowEditEndingEventArgs rowEditEndingEventArgs = new DataGridRowEditEndingEventArgs(cell.RowOwner, 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); if (!cell.RowOwner.IsEditing) { ReleaseCellAutomationValueHolders(); } } // 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 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 DataGridEditingUnit)) ? (DataGridEditingUnit)parameter : IsEditingCurrentCell ? DataGridEditingUnit.Cell : DataGridEditingUnit.Row; } /// /// Raised just before row editing is ended. /// Gives handlers the opportunity to cancel the operation. /// public event EventHandlerRowEditEnding; /// /// Called just before row editing is ended. /// Gives subclasses the opportunity to cancel the operation. /// protected virtual void OnRowEditEnding(DataGridRowEditEndingEventArgs e) { if (RowEditEnding != null) { RowEditEnding(this, e); } if (AutomationPeer.ListenerExists(AutomationEvents.InvokePatternOnInvoked)) { DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(this) as 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 EventHandlerCellEditEnding; /// /// Called just before cell editing is ended. /// Gives subclasses the opportunity to cancel the operation. /// protected virtual void OnCellEditEnding(DataGridCellEditEndingEventArgs e) { if (CellEditEnding != null) { CellEditEnding(this, e); } if (AutomationPeer.ListenerExists(AutomationEvents.InvokePatternOnInvoked)) { DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(this) as DataGridAutomationPeer; if (peer != null) { peer.RaiseAutomationCellInvokeEvents(e.Column, e.Row); } } } private static void OnCanExecuteCancelEdit(object sender, CanExecuteRoutedEventArgs e) { ((DataGrid)sender).OnCanExecuteCancelEdit(e); } private static void OnExecutedCancelEdit(object sender, ExecutedRoutedEventArgs e) { ((DataGrid)sender).OnExecutedCancelEdit(e); } ////// Invoked to determine if the CancelEdit command can be executed. /// protected virtual void OnCanExecuteCancelEdit(CanExecuteRoutedEventArgs e) { if (CanEndEdit(e, /* commit = */ false)) { e.CanExecute = true; e.Handled = true; } else { e.ContinueRouting = true; } } ////// Invoked when the CancelEdit command is executed. /// protected virtual void OnExecutedCancelEdit(ExecutedRoutedEventArgs e) { DataGridCell cell = CurrentCellContainer; if (cell != null) { DataGridEditingUnit editingUnit = GetEditingUnit(e.Parameter); bool eventCanceled = false; if (cell.IsEditing) { DataGridCellEditEndingEventArgs cellEditEndingEventArgs = new DataGridCellEditEndingEventArgs(cell.Column, cell.RowOwner, cell.EditingElement, DataGridEditAction.Cancel); OnCellEditEnding(cellEditEndingEventArgs); eventCanceled = cellEditEndingEventArgs.Cancel; if (!eventCanceled) { cell.CancelEdit(); HasCellValidationError = false; UpdateCellAutomationValueHolder(cell); } } if (!eventCanceled && IsAddingOrEditingRowItem(editingUnit, cell.RowDataItem)) { bool cancelAllowed = true; DataGridRowEditEndingEventArgs rowEditEndingEventArgs = new DataGridRowEditEndingEventArgs(cell.RowOwner, DataGridEditAction.Cancel); OnRowEditEnding(rowEditEndingEventArgs); cancelAllowed = !rowEditEndingEventArgs.Cancel; if (cancelAllowed) { var bindingGroup = cell.RowOwner.BindingGroup; if (bindingGroup != null) { bindingGroup.CancelEdit(); } CancelRowItem(); } } // 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; ReleaseCellAutomationValueHolders(); } // CancelEdit and CommitEdit rely on IsAddingNewItem and IsEditingRowItem CommandManager.InvalidateRequerySuggested(); } e.Handled = true; } private static void OnCanExecuteDelete(object sender, CanExecuteRoutedEventArgs e) { ((DataGrid)sender).OnCanExecuteDelete(e); } private static void OnExecutedDelete(object sender, ExecutedRoutedEventArgs e) { ((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 (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(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. SetCurrentValueInternal(CurrentItemProperty, itemToSelect); // Since the current cell should be in view, there should be a container 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(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 ((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(DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnCurrentItemChanged))); private static void OnCurrentItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = (DataGrid)d; DataGridCellInfo currentCell = dataGrid.CurrentCell; object newItem = e.NewValue; // the RowHeaders need to know when the current item changes so they can update their Visual State. OnNotifyRowHeaderPropertyChanged(d, e); if (currentCell.Item != newItem) { // Update the CurrentCell structure with the new item dataGrid.SetCurrentValueInternal(CurrentCellProperty, 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 DataGridColumn CurrentColumn { get { return (DataGridColumn)GetValue(CurrentColumnProperty); } set { SetValue(CurrentColumnProperty, value); } } ////// The DependencyProperty for CurrentColumn. /// public static readonly DependencyProperty CurrentColumnProperty = DependencyProperty.Register("CurrentColumn", typeof(DataGridColumn), typeof(DataGrid), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnCurrentColumnChanged))); private static void OnCurrentColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = (DataGrid)d; DataGridCellInfo currentCell = dataGrid.CurrentCell; DataGridColumn newColumn = (DataGridColumn)e.NewValue; if (currentCell.Column != newColumn) { // Update the CurrentCell structure with the new column dataGrid.SetCurrentValueInternal(CurrentCellProperty, 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 DataGridCellInfo CurrentCell { get { return (DataGridCellInfo)GetValue(CurrentCellProperty); } set { SetValue(CurrentCellProperty, value); } } ////// The DependencyProperty for CurrentCell. /// public static readonly DependencyProperty CurrentCellProperty = DependencyProperty.Register("CurrentCell", typeof(DataGridCellInfo), typeof(DataGrid), new FrameworkPropertyMetadata(DataGridCellInfo.Unset, new PropertyChangedCallback(OnCurrentCellChanged))); private static void OnCurrentCellChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = (DataGrid)d; DataGridCellInfo oldCell = (DataGridCellInfo)e.OldValue; DataGridCellInfo currentCell = (DataGridCellInfo)e.NewValue; if (dataGrid.CurrentItem != currentCell.Item) { dataGrid.SetCurrentValueInternal(CurrentItemProperty, currentCell.Item); } if (dataGrid.CurrentColumn != currentCell.Column) { dataGrid.SetCurrentValueInternal(CurrentColumnProperty, 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, DataGridEditingUnit.Row, /* exitEditingMode = */ true); } else if (dataGrid._currentCellContainer.IsEditing) { // Only the cell needs to commit. dataGrid.EndEdit(CommitEditCommand, dataGrid._currentCellContainer, DataGridEditingUnit.Cell, /* exitEditingMode = */ true); } } var oldCellContainer = dataGrid._currentCellContainer; 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. 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)) { if (!cell.IsKeyboardFocusWithin) { cell.Focus(); } if (oldCellContainer != cell) { if (oldCellContainer != null) { oldCellContainer.NotifyCurrentCellContainerChanged(); } cell.NotifyCurrentCellContainerChanged(); } } else if (oldCellContainer != null) { oldCellContainer.NotifyCurrentCellContainerChanged(); } } dataGrid.OnCurrentCellChanged(EventArgs.Empty); } ////// An event to notify that the value of CurrentCell changed. /// public event EventHandlerCurrentCellChanged; /// /// 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(DataGridCell cell, bool isFocusWithinCell) { if (isFocusWithinCell) { // 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 DataGridCell CurrentCellContainer { get { if (_currentCellContainer == null) { 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) { SetCurrentValueInternal(CurrentCellProperty, DataGridCellInfo.Unset); // ClearValue } else { SetCurrentValueInternal(CurrentCellProperty, new DataGridCellInfo(value)); } _pendingCurrentCellContainer = null; _currentCellContainer = value; CommandManager.InvalidateRequerySuggested(); } } } private bool IsEditingCurrentCell { get { DataGridCell cell = CurrentCellContainer; if (cell != null) { return cell.IsEditing; } return false; } } private bool IsCurrentCellReadOnly { get { 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 EventHandlerBeginningEdit; /// /// 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(DataGridBeginningEditEventArgs e) { if (BeginningEdit != null) { BeginningEdit(this, e); } if (AutomationPeer.ListenerExists(AutomationEvents.InvokePatternOnInvoked)) { DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(this) as 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 EventHandlerPreparingCellForEdit; /// /// 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(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 lead 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 lead 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) { 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(DataGridEditingUnit.Cell); } else if (IsEditingRowItem || IsAddingNewItem) { return CancelEdit(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(DataGridCell cell) { DataGridCell currentCell = CurrentCellContainer; if (currentCell != null && currentCell == cell && currentCell.IsEditing) { return CancelEdit(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(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(DataGridEditingUnit.Row); } else if (IsEditingCurrentCell) { // Cancel the current cell edit. CancelEdit(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(DataGridEditingUnit.Cell, true); } else if (IsEditingRowItem || IsAddingNewItem) { return CommitEdit(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(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(DataGridEditingUnit.Row, /* exitEditingMode = */ true); } else if (IsEditingCurrentCell) { // Commit the current cell edit. return CommitEdit(DataGridEditingUnit.Cell, /* exitEditingMode = */ true); } return true; } private bool EndEdit(RoutedCommand command, DataGridCell cellContainer, 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 == 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 == 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(); } } } ////// Cell in DataGrid which has logical focus /// internal DataGridCell FocusedCell { get { return _focusedCell; } set { if (_focusedCell != value) { if (_focusedCell != null) { UpdateCurrentCell(_focusedCell, false); } _focusedCell = value; if (_focusedCell != null) { UpdateCurrentCell(_focusedCell, true); } } } } #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(DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnCanUserAddRowsChanged), new CoerceValueCallback(OnCoerceCanUserAddRows))); private static void OnCanUserAddRowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).UpdateNewItemPlaceholder(/* isAddingNewItem = */ false); } private static object OnCoerceCanUserAddRows(DependencyObject d, object baseValue) { return OnCoerceCanUserAddOrDeleteRows((DataGrid)d, (bool)baseValue, /* canUserAddRowsProperty = */ true); } private static bool OnCoerceCanUserAddOrDeleteRows(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(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((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 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(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) { InitializingNewItemEventArgs e = new 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) { if (EditableItems.CanCancelEdit) { EditableItems.CancelEdit(); } else { // we haven't changed the data item, so this merely exits edit-mode EditableItems.CommitEdit(); } } else { object currentAddItem = EditableItems.CurrentAddItem; bool wasCurrent = currentAddItem == CurrentItem; bool wasSelected = SelectedItems.Contains(currentAddItem); bool reselectPlaceholderCells = false; ListcolumnIndexRanges = 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 SetCurrentValueInternal(CurrentItemProperty, 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(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 IsAddingOrEditingRowItem(DataGridEditingUnit editingUnit, object item) { return (editingUnit == DataGridEditingUnit.Row) && IsAddingOrEditingRowItem(item); } private bool IsEditingItem(object item) { return IsEditingRowItem && (EditableItems.CurrentEditItem == item); } private void UpdateNewItemPlaceholder(bool isAddingNewItem) { var editableItems = EditableItems; bool canUserAddRows = CanUserAddRows; if (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 DataGridRow newItemPlaceholderRow = (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) { SetCurrentValueInternal(CurrentItemProperty, Items[itemCount - 1]); } } else if (position == NewItemPlaceholderPosition.AtBeginning) { if (Items.Count > 0) { SetCurrentValueInternal(CurrentItemProperty, 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 == DataGrid.NewItemPlaceholder); } #endregion #region Row Details /// /// Determines which visibility mode the Row's details use. /// public DataGridRowDetailsVisibilityMode RowDetailsVisibilityMode { get { return (DataGridRowDetailsVisibilityMode)GetValue(RowDetailsVisibilityModeProperty); } set { SetValue(RowDetailsVisibilityModeProperty, value); } } ////// DependencyProperty for RowDetailsVisibilityMode. /// public static readonly DependencyProperty RowDetailsVisibilityModeProperty = DependencyProperty.Register("RowDetailsVisibilityMode", typeof(DataGridRowDetailsVisibilityMode), typeof(DataGrid), new FrameworkPropertyMetadata(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(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(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(DataGrid), new FrameworkPropertyMetadata(null, OnNotifyRowAndDetailsPropertyChanged)); ////// Event that is fired just before the details of a Row is shown /// public event EventHandlerLoadingRowDetails; /// /// Event that is fired just before the details of a Row is hidden /// public event EventHandlerUnloadingRowDetails; /// /// Event that is fired when the visibility of a Rows details changes. /// public event EventHandlerRowDetailsVisibilityChanged; internal void OnLoadingRowDetailsWrapper(DataGridRow row) { if (row != null && row.DetailsLoaded == false && row.DetailsVisibility == Visibility.Visible && row.DetailsPresenter != null) { DataGridRowDetailsEventArgs e = new DataGridRowDetailsEventArgs(row, row.DetailsPresenter.DetailsElement); OnLoadingRowDetails(e); row.DetailsLoaded = true; } } internal void OnUnloadingRowDetailsWrapper(DataGridRow row) { if (row != null && row.DetailsLoaded == true && row.DetailsPresenter != null) { DataGridRowDetailsEventArgs e = new DataGridRowDetailsEventArgs(row, row.DetailsPresenter.DetailsElement); OnUnloadingRowDetails(e); row.DetailsLoaded = false; } } /// /// Invokes the LoadingRowDetails event /// protected virtual void OnLoadingRowDetails(DataGridRowDetailsEventArgs e) { if (LoadingRowDetails != null) { LoadingRowDetails(this, e); } } ////// Invokes the UnloadingRowDetails event /// protected virtual void OnUnloadingRowDetails(DataGridRowDetailsEventArgs e) { if (UnloadingRowDetails != null) { UnloadingRowDetails(this, e); } } ////// Invokes the RowDetailsVisibilityChanged event /// protected internal virtual void OnRowDetailsVisibilityChanged(DataGridRowDetailsEventArgs e) { if (RowDetailsVisibilityChanged != null) { RowDetailsVisibilityChanged(this, e); } var row = e.Row; // LoadingRowDetails only needs to be called when row.DetailsVisibility == Visibility.Visible. // OnLoadingRowDetailsWrapper already makes this check, so we omit it here. // // No need to used DelayedOnLoadingRowDetails because OnRowDetailsVisibilityChanged isn't called until after the // template is expanded. OnLoadingRowDetailsWrapper(row); } #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(DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnNotifyRowHeaderPropertyChanged))); #endregion #region Selection ////// The currently selected cells. /// public IListSelectedCells { get { return _selectedCells; } } internal 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, VirtualizedCellInfoCollection oldItems, VirtualizedCellInfoCollection newItems) { DataGridSelectionMode selectionMode = SelectionMode; DataGridSelectionUnit selectionUnit = SelectionUnit; if (!IsUpdatingSelectedCells && (selectionUnit == 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) { 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) { 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 == 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 == 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 (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 SelectedCellsChangedEventArgs e = new 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(SelectedCellsChangedEventArgs e) { if (SelectedCellsChanged != null) { SelectedCellsChanged(this, e); } // Raise automation events if (AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementSelected) || AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementAddedToSelection) || AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection)) { DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(this) as DataGridAutomationPeer; if (peer != null) { peer.RaiseAutomationCellSelectedEvent(e); } } } ////// A command that, when invoked, will select all items in the DataGrid. /// public static RoutedUICommand SelectAllCommand { get { return ApplicationCommands.SelectAll; } } private static void OnCanExecuteSelectAll(object sender, CanExecuteRoutedEventArgs e) { DataGrid dataGrid = (DataGrid)sender; e.CanExecute = (dataGrid.SelectionMode == DataGridSelectionMode.Extended) && dataGrid.IsEnabled; e.Handled = true; } private static void OnExecutedSelectAll(object sender, ExecutedRoutedEventArgs e) { DataGrid dataGrid = (DataGrid)sender; if (dataGrid.SelectionUnit == DataGridSelectionUnit.Cell) { dataGrid.SelectAllCells(); } else { dataGrid.SelectAll(); } e.Handled = true; } ////// Called from the public SelectAll API. /// internal override void SelectAllImpl() { int numItems = Items.Count; int numColumns = _columns.Count; if ((numColumns > 0) && (numItems > 0)) { using (UpdateSelectedCells()) { // Selecting the cells first is an optimization. _selectedCells.AddRegion(0, 0, numItems, numColumns); base.SelectAllImpl(); } } } internal void SelectOnlyThisCell(DataGridCellInfo currentCellInfo) { using (UpdateSelectedCells()) { _selectedCells.Clear(); _selectedCells.Add(currentCellInfo); } } ////// Selects all cells. /// public void SelectAllCells() { if (SelectionUnit == DataGridSelectionUnit.FullRow) { SelectAll(); } 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() { using (UpdateSelectedCells()) { // Unselect all of the cells _selectedCells.Clear(); if (SelectionUnit != 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 DataGridSelectionMode SelectionMode { get { return (DataGridSelectionMode)GetValue(SelectionModeProperty); } set { SetValue(SelectionModeProperty, value); } } ////// The DependencyProperty for the SelectionMode property. /// public static readonly DependencyProperty SelectionModeProperty = DependencyProperty.Register("SelectionMode", typeof(DataGridSelectionMode), typeof(DataGrid), new FrameworkPropertyMetadata(DataGridSelectionMode.Extended, new PropertyChangedCallback(OnSelectionModeChanged))); private static void OnSelectionModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = (DataGrid)d; DataGridSelectionMode newSelectionMode = (DataGridSelectionMode)e.NewValue; bool changingToSingleMode = newSelectionMode == DataGridSelectionMode.Single; DataGridSelectionUnit selectionUnit = dataGrid.SelectionUnit; if (changingToSingleMode && (selectionUnit == 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 != DataGridSelectionMode.Single); if (changingToSingleMode && (selectionUnit == 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 DataGridSelectionUnit SelectionUnit { get { return (DataGridSelectionUnit)GetValue(SelectionUnitProperty); } set { SetValue(SelectionUnitProperty, value); } } ////// The DependencyProperty for the SelectionUnit property. /// public static readonly DependencyProperty SelectionUnitProperty = DependencyProperty.Register("SelectionUnit", typeof(DataGridSelectionUnit), typeof(DataGrid), new FrameworkPropertyMetadata(DataGridSelectionUnit.FullRow, new PropertyChangedCallback(OnSelectionUnitChanged))); private static void OnSelectionUnitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = (DataGrid)d; DataGridSelectionUnit oldUnit = (DataGridSelectionUnit)e.OldValue; // Full wipe on unit change if (oldUnit != DataGridSelectionUnit.Cell) { dataGrid.UnselectAll(); } if (oldUnit != 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)) { DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(this) as 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(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; ContainerTrackingrowTracker = _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) { ContainerTracking rowTracker = _rowTrackingRoot; while (rowTracker != null) { DataGridRow row = rowTracker.Container; DataGridCellsPresenter cellsPresenter = row.CellsPresenter; if (cellsPresenter != null) { ContainerTracking cellTracker = cellsPresenter.CellTrackingRoot; while (cellTracker != null) { DataGridCell cell = cellTracker.Container; DataGridCellInfo cellInfo = new DataGridCellInfo(cell); if (cells.Contains(cellInfo)) { cell.SyncIsSelected(isSelected); } cellTracker = cellTracker.Next; } } rowTracker = rowTracker.Next; } } else { foreach (DataGridCellInfo cellInfo in cells) { 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(DataGridCell cell, bool isSelected) { if (!IsUpdatingSelectedCells) { DataGridCellInfo cellInfo = new 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(DataGridCell cell, bool startDragging, bool allowsExtendSelect, bool allowsMinimalSelect) { DataGridSelectionUnit selectionUnit = SelectionUnit; // If the mode is None, then no selection will occur if (selectionUnit == 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 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(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(); } DataGridCellInfo currentCell = CurrentCell; if (currentCell.Item != rowItem) { // Change the CurrentCell if the row is different SetCurrentValueInternal(CurrentCellProperty, new DataGridCellInfo(rowItem, ColumnFromDisplayIndex(0), this)); } else { if (_currentCellContainer != null && _currentCellContainer.IsEditing) { // End the pending edit even for the same row EndEdit(CommitEditCommand, _currentCellContainer, DataGridEditingUnit.Cell, /* exitEditingMode = */ true); } } } // 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 workaround, 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 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(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 workaround, see remarks. if (_selectionAnchor.Value.Item == _editingRowItem) { startIndex = _editingRowIndex; } if (cellInfo.Item == _editingRowItem) { endIndex = _editingRowIndex; } } 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 workaround, 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 == 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 workaround, see remarks. selectedCellsContainsCellInfo = _selectedCells.Contains(_editingRowIndex, cellInfoColumnIndex); } if (minimalModify && selectedCellsContainsCellInfo) { // Unselect the one cell if (singleRowOperation) { // ADO.Net bug workaround, see remarks. _selectedCells.RemoveRegion(_editingRowIndex, cellInfoColumnIndex, 1, 1); } else { _selectedCells.Remove(cellInfo); } if ((SelectionUnit == 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 workaround, 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(DataGrid dataGrid) { _dataGrid = dataGrid; _wasUpdatingSelectedCells = _dataGrid.IsUpdatingSelectedCells; if (!_wasUpdatingSelectedCells) { _dataGrid.BeginUpdateSelectedCells(); } } public void Dispose() { GC.SuppressFinalize(this); if (!_wasUpdatingSelectedCells) { _dataGrid.EndUpdateSelectedCells(); } } private 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 DataGridSelectionUnit.FullRow: case DataGridSelectionUnit.CellOrRowHeader: return true; case 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); DataGrid dataGrid = (DataGrid)d; if (!(bool)(e.NewValue)) { dataGrid.UnselectAllCells(); } // Many commands use IsEnabled to determine if they are enabled or not CommandManager.InvalidateRequerySuggested(); dataGrid.UpdateVisualState(); } ////// Cells need to update their visual state when this property changes. /// /// /// private static void OnIsKeyboardFocusWithinChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.Rows | DataGridNotificationTarget.RowHeaders | DataGridNotificationTarget.Cells); } ////// 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; } } ////// Helper method which handles the arrow key down /// ////// 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 OnArrowKeyDown(KeyEventArgs e) { 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; object currentItem = CurrentItem; int currentRowIndex = Items.IndexOf(currentItem); // ADO.Net bug workaround, see remarks. if (_editingRowIndex >= 0 && currentItem == _editingRowItem) { currentRowIndex = _editingRowIndex; } 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) { 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) { 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; } DataGridColumn nextColumn = ColumnFromDisplayIndex(nextDisplayIndex); object nextItem = Items[nextRowIndex]; ScrollCellIntoView(nextItem, nextColumn); 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. 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); } // 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) { DataGridCell newCell = GetCellForSelectAndEditOnFocusMove(); if (newCell != null && newCell.RowDataItem == currentCellContainer.RowDataItem) { 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) { DataGridCell currentCellContainer = CurrentCellContainer; if ((currentCellContainer != null) && (_columns.Count > 0)) { e.Handled = true; 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 SetCurrentValueInternal(CurrentCellProperty, new 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 DataGridCell GetCellForSelectAndEditOnFocusMove() { DataGridCell newCell = Keyboard.FocusedElement as 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, DataGridCell oldCell, bool wasEditing, bool allowsExtendSelect, bool ignoreControlKey) { DataGridCell newCell = GetCellForSelectAndEditOnFocusMove(); if ((newCell != null) && (newCell.DataGridOwner == this)) { if (ignoreControlKey || ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == 0)) { if (ShouldSelectRowHeader && allowsExtendSelect) { HandleSelectionForRowHeaderAndDetailsInput(newCell.RowOwner, /* startDragging = */ false); } else { 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; DataGridColumn column = ColumnFromDisplayIndex(homeKey ? InternalColumns.FirstVisibleDisplayIndex : InternalColumns.LastVisibleDisplayIndex); ScrollCellIntoView(item, column); DataGridCell cell = TryFindCell(item, column); if (cell != null) { cell.Focus(); if (ShouldSelectRowHeader) { HandleSelectionForRowHeaderAndDetailsInput(cell.RowOwner, /* startDragging = */ false); } else { 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; 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]; if (currentColumn == null) { ScrollRowIntoView(targetRow); SetCurrentValueInternal(CurrentItemProperty, targetRow); } else { ScrollCellIntoView(targetRow, currentColumn); DataGridCell cell = TryFindCell(targetRow, currentColumn); if (cell != null) { cell.Focus(); if (ShouldSelectRowHeader) { HandleSelectionForRowHeaderAndDetailsInput(cell.RowOwner, /* startDragging = */ false); } else { 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) { DataGridRow row = MouseOverRow; if ((row != null) && (row.Item != CurrentItem)) { // Continue a row header drag to the given row HandleSelectionForRowHeaderAndDetailsInput(row, /* startDragging = */ false); SetCurrentValueInternal(CurrentItemProperty, row.Item); e.Handled = true; } } else { DataGridCell cell = MouseOverCell; if (cell == null) { 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 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); SetCurrentValueInternal(CurrentItemProperty, 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 StartAutoScroll(); } } } } else { // The mouse button is up, end the drag operation EndDragging(); } } } private static void OnAnyMouseUpThunk(object sender, MouseButtonEventArgs e) { ((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) { DataGridCell cell = null; DataGridRowHeader rowHeader = null; UIElement sourceElement = e.OriginalSource as UIElement; while (sourceElement != null) { cell = sourceElement as DataGridCell; if (cell != null) { break; } rowHeader = sourceElement as 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) { 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 DataGridRow GetRowNearMouse() { Debug.Assert(RelativeMousePosition != RelativeMousePositions.Over, "The mouse is not supposed to be over the DataGrid."); Panel itemsHost = InternalItemsHost; if (itemsHost != null) { bool isGrouping = IsGrouping; // Iterate from the end to the beginning since it is more common // to drag toward the end. for (int i = (isGrouping ? Items.Count - 1 : itemsHost.Children.Count - 1); i >= 0; i--) { DataGridRow row = null; if (isGrouping) { // If Grouping is enabled, Children of itemsHost are not always DataGridRows row = ItemContainerGenerator.ContainerFromIndex(i) as DataGridRow; } else { row = itemsHost.Children[i] as 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 DataGridCell GetCellNearMouse() { Panel itemsHost = InternalItemsHost; if (itemsHost != null) { Rect itemsHostBounds = new Rect(new Point(), itemsHost.RenderSize); double closestDistance = Double.PositiveInfinity; DataGridCell closestCell = null; bool isMouseInCorner = IsMouseInCorner(RelativeMousePosition); bool isGrouping = IsGrouping; // Iterate from the end to the beginning since it is more common // to drag toward the end. for (int i = (isGrouping ? Items.Count - 1 : itemsHost.Children.Count - 1); i >= 0; i--) { DataGridRow row = null; if (isGrouping) { // If Grouping is enabled, Children of itemsHost are not always DataGridRows row = ItemContainerGenerator.ContainerFromIndex(i) as DataGridRow; } else { row = itemsHost.Children[i] as DataGridRow; } if (row != null) { DataGridCellsPresenter cellsPresenter = row.CellsPresenter; if (cellsPresenter != null) { // Go through all of the instantiated cells and find the closest cell ContainerTrackingcellTracker = cellsPresenter.CellTrackingRoot; while (cellTracker != null) { 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 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 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, 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 DataGridRow MouseOverRow { get { return DataGridHelper.FindVisualParent(Mouse.DirectlyOver as UIElement); } } // The cell that the mouse is over. private static DataGridCell MouseOverCell { get { return 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 System.Windows.Automation.Peers.DataGridAutomationPeer(this); } private CellAutomationValueHolder GetCellAutomationValueHolder(object item, DataGridColumn column) { CellAutomationValueHolder cellAutomationValueHolder; if (!Object.Equals(item, _editingRowItem) || !_editingCellAutomationValueHolders.TryGetValue(column, out cellAutomationValueHolder)) { DataGridCell cell = TryFindCell(item, column); cellAutomationValueHolder = (cell != null) ? new CellAutomationValueHolder(cell) : new CellAutomationValueHolder(item, column); } return cellAutomationValueHolder; } internal string GetCellAutomationValue(object item, DataGridColumn column) { CellAutomationValueHolder cellAutomationValueHolder = GetCellAutomationValueHolder(item, column); return cellAutomationValueHolder.Value; } internal object GetCellClipboardValue(object item, DataGridColumn column) { CellAutomationValueHolder cellAutomationValueHolder = GetCellAutomationValueHolder(item, column); return cellAutomationValueHolder.GetClipboardValue(); } internal void SetCellAutomationValue(object item, DataGridColumn column, string value) { SetCellValue(item, column, value, false /*clipboard*/); } internal void SetCellClipboardValue(object item, DataGridColumn column, object value) { SetCellValue(item, column, value, true /*clipboard*/); } private void SetCellValue(object item, DataGridColumn column, object value, bool clipboard) { // Put focus on the cell CurrentCellContainer = TryFindCell(item, column); if (CurrentCellContainer == null) { // If current cell has been virtualized away - scroll it into view ScrollCellIntoView(item, column); CurrentCellContainer = TryFindCell(item, column); } if (CurrentCellContainer == null) { return; } // Check if trying to edit cell while previously edited cell has an error. // BeginEdit will fail if there is a validation error. if (BeginEdit()) { CellAutomationValueHolder holder; if (_editingCellAutomationValueHolders.TryGetValue(column, out holder)) { holder.SetValue(this, value, clipboard); // calls CommitEdit } else { // if there's no automation value-holder, we can't honor the SetValue request, // so just cancel the edit CancelEdit(); } } } // create an automation value-holder for the given cell private void EnsureCellAutomationValueHolder(DataGridCell cell) { if (!_editingCellAutomationValueHolders.ContainsKey(cell.Column)) { _editingCellAutomationValueHolders.Add(cell.Column, new CellAutomationValueHolder(cell)); } } // update the automation/clipboard value for the given cell private void UpdateCellAutomationValueHolder(DataGridCell cell) { CellAutomationValueHolder holder; if (_editingCellAutomationValueHolders.TryGetValue(cell.Column, out holder)) { holder.TrackValue(); } } // release the automation value-holders, after notifying them one final time. // This is called at CommitEdit and CancelEdit time. private void ReleaseCellAutomationValueHolders() { foreach (KeyValuePairkvp in _editingCellAutomationValueHolders) { kvp.Value.TrackValue(); } _editingCellAutomationValueHolders.Clear(); } #region CellAutomationHelper internal class CellAutomationValueHolder { // Track the value of an actual cell public CellAutomationValueHolder(DataGridCell cell) { _cell = cell; Initialize(cell.RowDataItem, cell.Column); } // Track the value of an (item,column) that has no corresponding cell. // This is used only to get the value from a virtualized item. public CellAutomationValueHolder(object item, DataGridColumn column) { Initialize(item, column); } private void Initialize(object item, DataGridColumn column) { _item = item; _column = column; _value = GetValue(); } public string Value { get { return _value; } } // called by DataGrid when the value has potentially changed public void TrackValue() { string newValue = GetValue(); if (newValue != _value) { if (AutomationPeer.ListenerExists(AutomationEvents.PropertyChanged)) { DataGridColumn column = (_cell != null) ? _cell.Column : _column; DataGridAutomationPeer peer = DataGridAutomationPeer.FromElement(column.DataGridOwner) as DataGridAutomationPeer; if (peer != null) { object item = (_cell != null) ? _cell.DataContext : _item; DataGridItemAutomationPeer dataGridItemAutomationPeer = peer.FindOrCreateItemAutomationPeer(item) as DataGridItemAutomationPeer; if (dataGridItemAutomationPeer != null) { DataGridCellItemAutomationPeer cellPeer = dataGridItemAutomationPeer.GetOrCreateCellItemPeer(column); if (cellPeer != null) { cellPeer.RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, _value, newValue); } } } } _value = newValue; } } // get the current value private string GetValue() { string value; if (_column.ClipboardContentBinding == null) { value = null; } else if (_inSetValue) { // when setting the value from automation, there's a re-entrant call: // SetValue->CommitEdit->TrackValue->GetValue. // Inside this re-entrant call, there's already a binding in place, // and it has the value we want. value = (string)_cell.GetValue(CellContentProperty); } else { FrameworkElement target; if (_cell != null) { // Bind the CellContent property of the cell itself. // The binding will participate in the row's BindingGroup, and // pick up a proposed value (if any). target = _cell; } else { // lacking a cell, bind a dummy element directly to the data item target = new FrameworkElement(); target.DataContext = _item; } BindingOperations.SetBinding(target, CellContentProperty, _column.ClipboardContentBinding); value = (string)target.GetValue(CellContentProperty); BindingOperations.ClearBinding(target, CellContentProperty); } return value; } // get the current clipboard value. This is similar to the "content" value, // except that it has type Object (rather than String). We don't track // the clipboard value, since no one is interested in changes. Instead, // we recompute it every time it's requested. public object GetClipboardValue() { object value; if (_column.ClipboardContentBinding == null) { value = null; } else { FrameworkElement target; if (_cell != null) { // Bind the CellClipboard property of the cell itself. // The binding will participate in the row's BindingGroup, and // pick up a proposed value (if any). target = _cell; } else { // lacking a cell, bind a dummy element directly to the data item target = new FrameworkElement(); target.DataContext = _item; } BindingOperations.SetBinding(target, CellClipboardProperty, _column.ClipboardContentBinding); value = target.GetValue(CellClipboardProperty); BindingOperations.ClearBinding(target, CellClipboardProperty); } return value; } // set the value (used when setting value via automation) public void SetValue(DataGrid dataGrid, object value, bool clipboard) { if (_column.ClipboardContentBinding == null) return; _inSetValue = true; // add a two-way binding (it joins the BindingGroup) DependencyProperty dp = clipboard ? CellClipboardProperty : CellContentProperty; BindingBase binding = _column.ClipboardContentBinding.Clone(BindingMode.TwoWay); BindingOperations.SetBinding(_cell, dp, binding); // set the new value _cell.SetValue(dp, value); // do a cell-level commit - this will validate the new value dataGrid.CommitEdit(); // whether valid or not, remove the binding. The binding group will // remember the proposed value BindingOperations.ClearBinding(_cell, dp); _inSetValue = false; } private static DependencyProperty CellContentProperty = DependencyProperty.RegisterAttached("CellContent", typeof(string), typeof(CellAutomationValueHolder)); private static DependencyProperty CellClipboardProperty = DependencyProperty.RegisterAttached("CellClipboard", typeof(object), typeof(CellAutomationValueHolder)); # region Data private DataGridCell _cell; private DataGridColumn _column; private object _item; private string _value; private bool _inSetValue; #endregion } #endregion #endregion #region Cell Info private DataGridCell TryFindCell(DataGridCellInfo info) { // Does not de-virtualize cells return TryFindCell(info.Item, info.Column); } internal DataGridCell TryFindCell(object item, DataGridColumn column) { // Does not de-virtualize cells DataGridRow row = (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(DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnCanUserSortColumnsPropertyChanged), new CoerceValueCallback(OnCoerceCanUserSortColumns))); ////// 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); } } private static object OnCoerceCanUserSortColumns(DependencyObject d, object baseValue) { DataGrid dataGrid = (DataGrid)d; if( DataGridHelper.IsPropertyTransferEnabled(dataGrid, CanUserSortColumnsProperty) && DataGridHelper.IsDefaultValue(dataGrid, CanUserSortColumnsProperty) && dataGrid.Items.CanSort == false) { return false; } return baseValue; } private static void OnCanUserSortColumnsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = (DataGrid)d; DataGridHelper.TransferProperty(dataGrid, CanUserSortColumnsProperty); OnNotifyColumnPropertyChanged(d, e); } public event DataGridSortingEventHandler Sorting; ////// Protected method which raises the sorting event and does default sort /// /// protected virtual void OnSorting(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(DataGridColumn sortColumn) { Debug.Assert(sortColumn != null, "column should not be null"); if (!CanUserSortColumns || !sortColumn.CanUserSort) { return; } if (CommitAnyEdit()) { PrepareForSort(sortColumn); DataGridSortingEventArgs eventArgs = new 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(DataGridColumn sortColumn) { if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) { return; } if (Columns != null) { foreach (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(DataGridColumn column, bool clearExistingSortDescriptions) { ListSortDirection sortDirection = ListSortDirection.Ascending; NullablecurrentSortDirection = 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 ListGroupingSortDescriptionIndices { get { return _groupingSortDescriptionIndices; } set { _groupingSortDescriptionIndices = value; } } /// /// SortDescription collection changed listener. Ensures that GroupingSortDescriptionIndices /// is in [....] 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 EventHandlerAutoGeneratingColumn; /// /// The DependencyProperty that represents the AutoGenerateColumns property. /// public static readonly DependencyProperty AutoGenerateColumnsProperty = DependencyProperty.Register("AutoGenerateColumns", typeof(bool), typeof(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(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); // always use an ItemBindingGroup EnsureItemBindingGroup(); // always turn on SharesProposedValues ItemBindingGroup.SharesProposedValues = true; } else if (DeferAutoGeneration && AutoGenerateColumns) { // Try to generate auto columns if it was deferred earlier. AddAutoColumns(); } return base.MeasureOverride(availableSize); } // Set the ItemBindingGroup property, if the user hasn't done so already private void EnsureItemBindingGroup() { if (ItemBindingGroup == null) { _defaultBindingGroup = new BindingGroup(); SetCurrentValue(ItemBindingGroupProperty, _defaultBindingGroup); } } ////// Helper method to clear SortDescriptions and all related /// member when ItemsSource changes /// private void ClearSortDescriptionsOnItemsSourceChange() { Items.SortDescriptions.Clear(); _sortingStarted = false; ListgroupingSortDescriptionIndices = GroupingSortDescriptionIndices; if (groupingSortDescriptionIndices != null) { groupingSortDescriptionIndices.Clear(); } foreach (DataGridColumn column in Columns) { column.SortDirection = null; } } /// /// 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) { DataGrid dataGrid = (DataGrid)d; if (baseValue != dataGrid._cachedItemsSource && dataGrid._cachedItemsSource != null) { dataGrid.ClearSortDescriptionsOnItemsSourceChange(); } 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) { ClearSortDescriptionsOnItemsSourceChange(); } _cachedItemsSource = newValue; using (UpdateSelectedCells()) { // Selector will try to maintain the previous row selection. // Keep SelectedCells in [....]. _selectedCells.RestoreOnlyFullRows(SelectedItems); } if (AutoGenerateColumns == true) { RegenerateAutoColumns(); } InternalColumns.RefreshAutoWidthColumns = true; InternalColumns.InvalidateColumnWidthsComputation(); CoerceValue(CanUserAddRowsProperty); CoerceValue(CanUserDeleteRowsProperty); DataGridHelper.TransferProperty(this, CanUserSortColumnsProperty); ResetRowHeaderActualWidth(); UpdateNewItemPlaceholder(/* isAddingNewItem = */ false); HasCellValidationError = false; HasRowValidationError = false; } ////// The flag which determines whether the columns generation is deferred /// private bool DeferAutoGeneration { get; set; } ////// Performs column auto generation and updates validation flags /// on items change. /// /// protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) { base.OnItemsChanged(e); if (e.Action == NotifyCollectionChangedAction.Add) { if (DeferAutoGeneration) { // Add Auto columns only if it was deferred earlier AddAutoColumns(); } } 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 (DataItemsCount == 0) { // do deferred generation DeferAutoGeneration = true; } else if (!_measureNeverInvoked) { DataGrid.GenerateColumns( (IItemProperties)Items, this, null); DeferAutoGeneration = false; 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 (); 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, DataGrid dataGrid, CollectioncolumnCollection) { 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) { DataGridColumn dataGridColumn = 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. DataGridAutoGeneratingColumnEventArgs eventArgs = new 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; DataGrid dataGrid = (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(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) { DataGrid dataGrid = (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) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.ColumnCollection | DataGridNotificationTarget.ColumnHeadersPresenter | DataGridNotificationTarget.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(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(DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnEnableRowVirtualizationChanged))); ////// Property changed callback for EnableRowVirtualization. /// Keeps VirtualizingStackPanel.IsVirtualizingProperty in [....]. /// private static void OnEnableRowVirtualizationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataGrid dataGrid = (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 (!DataGridHelper.IsDefaultValue(d, DataGrid.EnableRowVirtualizationProperty)) { return d.GetValue(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(DataGrid), new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnEnableColumnVirtualizationChanged))); ////// Property changed callback for EnableColumnVirtualization. /// Gets VirtualizingStackPanel.IsVirtualizingProperty for cells presenter and /// headers presenter in [....]. /// private static void OnEnableColumnVirtualizationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.CellsPresenter | DataGridNotificationTarget.ColumnHeadersPresenter | DataGridNotificationTarget.ColumnCollection); } #endregion #region Column Reordering ////// Dependency Property for CanUserReorderColumns Property /// public static readonly DependencyProperty CanUserReorderColumnsProperty = DependencyProperty.Register("CanUserReorderColumns", typeof(bool), typeof(DataGrid), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnNotifyColumnPropertyChanged))); ////// 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(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(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 EventHandlerColumnReordering; 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(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(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(DataGridClipboardCopyMode), typeof(DataGrid), new FrameworkPropertyMetadata(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 DataGridClipboardCopyMode ClipboardCopyMode { get { return (DataGridClipboardCopyMode)GetValue(ClipboardCopyModeProperty); } set { SetValue(ClipboardCopyModeProperty, value); } } private static void OnCanExecuteCopy(object target, CanExecuteRoutedEventArgs args) { ((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 != DataGridClipboardCopyMode.None && _selectedCells.Count > 0; args.Handled = true; } private static void OnExecutedCopy(object target, ExecutedRoutedEventArgs args) { ((DataGrid)target).OnExecutedCopy(args); } ////// This virtual method is called when ApplicationCommands.Copy command is executed. /// /// ////// Critical: Calls Secuirty Critical Clipboard operations in full and partial trust. /// TreatAsSafe: Only puts text on the clipboard in full trust or a userinitiated command in partial trust through secured critical functions. /// [SecurityCritical, SecurityTreatAsSafe] protected virtual void OnExecutedCopy(ExecutedRoutedEventArgs args) { if (ClipboardCopyMode == DataGridClipboardCopyMode.None) { throw new NotSupportedException(SR.Get(SRID.ClipboardCopyMode_Disabled)); } args.Handled = true; // Supported default formats: Html, Text, UnicodeText and CSV Collectionformats = 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 == DataGridClipboardCopyMode.IncludeHeader) { DataGridRowClipboardEventArgs preparingRowClipboardContentEventArgs = new 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)) { DataGridRowClipboardEventArgs preparingRowClipboardContentEventArgs = new DataGridRowClipboardEventArgs(row, minColumnDisplayIndex, maxColumnDisplayIndex, false /*IsColumnHeadersRow*/, i); OnCopyingRowClipboardContent(preparingRowClipboardContentEventArgs); foreach (string format in formats) { dataGridStringBuilders[format].Append(preparingRowClipboardContentEventArgs.FormatClipboardCellValues(format)); } } } } DataGridClipboardHelper.GetClipboardContentForHtml(dataGridStringBuilders[DataFormats.Html]); DataObject dataObject; bool hasPerms = SecurityHelper.CallerHasAllClipboardPermission() && SecurityHelper.CallerHasSerializationPermission(); // Copy unconditionally in full trust. // Only copy in partial trust if user initiated. if (hasPerms || args.UserInitiated ) { (new UIPermission(UIPermissionClipboard.AllClipboard)).Assert(); try { dataObject = new DataObject(); } finally { UIPermission.RevertAssert(); } foreach (string format in formats) { dataObject.CriticalSetData(format, dataGridStringBuilders[format].ToString(), false /*autoConvert*/); } // This assert is there for an OLE Callback to register CSV type for the clipboard (new SecurityPermission(SecurityPermissionFlag.SerializationFormatter | SecurityPermissionFlag.UnmanagedCode)).Assert(); try { Clipboard.CriticalSetDataObject(dataObject, true /* Copy */); } finally { SecurityPermission.RevertAll(); } } } /// /// 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(DataGridRowClipboardEventArgs args) { if (args.IsColumnHeadersRow) { for (int i = args.StartColumnDisplayIndex; i <= args.EndColumnDisplayIndex; i++) { DataGridColumn column = ColumnFromDisplayIndex(i); if (!column.IsVisible) { continue; } args.ClipboardRowContent.Add(new 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++) { 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 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 EventHandlerCopyingRowClipboardContent; #endregion #region Cells Panel Width /// /// Dependency Property for CellsPanelActualWidth property /// internal static readonly DependencyProperty CellsPanelActualWidthProperty = DependencyProperty.Register( "CellsPanelActualWidth", typeof(double), typeof(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)) { ((DataGrid)d).NotifyPropertyChanged(d, e, DataGridNotificationTarget.ColumnHeadersPresenter); } } #endregion #region Column Width Computations ////// Dependency Property Key for CellsPanelHorizontalOffset property /// private static readonly DependencyPropertyKey CellsPanelHorizontalOffsetPropertyKey = DependencyProperty.RegisterReadOnly( "CellsPanelHorizontalOffset", typeof(double), typeof(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; } IProvideDataGridColumn cell = GetAnyCellOrColumnHeader(); if (cell != null) { CellsPanelHorizontalOffset = DataGridHelper.GetParentCellsPanelHorizontalOffset(cell); } else if (!Double.IsNaN(RowHeaderWidth)) { CellsPanelHorizontalOffset = RowHeaderWidth; } else { CellsPanelHorizontalOffset = 0d; } CellsPanelHorizontalOffsetComputationPending = false; return null; } ////// Helper method which return any one of the cells or column headers /// ///internal IProvideDataGridColumn GetAnyCellOrColumnHeader() { // Find the best try at a visible cell from a visible row. if (_rowTrackingRoot != null) { ContainerTracking rowTracker = _rowTrackingRoot; while (rowTracker != null) { if (rowTracker.Container.IsVisible) { DataGridCellsPresenter cellsPresenter = rowTracker.Container.CellsPresenter; if (cellsPresenter != null) { ContainerTracking cellTracker = cellsPresenter.CellTrackingRoot; while (cellTracker != null) { if (cellTracker.Container.IsVisible) { return cellTracker.Container; } cellTracker = cellTracker.Next; } } } rowTracker = rowTracker.Next; } } // If the row that we found earlier is not a good choice try a column header. // If no good column header is found the fall back will be the cell. if (ColumnHeadersPresenter != null) { ContainerTracking headerTracker = ColumnHeadersPresenter.HeaderTrackingRoot; while (headerTracker != null) { if (headerTracker.Container.IsVisible) { return headerTracker.Container; } headerTracker = headerTracker.Next; } } 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 Visual States internal override void ChangeVisualState(bool useTransitions) { if (!IsEnabled) { VisualStates.GoToState(this, useTransitions, VisualStates.StateDisabled, VisualStates.StateNormal); } else { VisualStates.GoToState(this, useTransitions, VisualStates.StateNormal); } base.ChangeVisualState(useTransitions); } #endregion #region Helpers // internal static object NewItemPlaceholder { get { return _newItemPlaceholder; } } #endregion #region Data 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 NamedObject("DataGrid.NewItemPlaceholder"); // Used as an alternate data item to CollectionView.NewItemPlaceholder private DataGridColumnCollection _columns; // Stores the columns private ContainerTracking _rowTrackingRoot; // Root of a linked list of active row containers private DataGridColumnHeadersPresenter _columnHeadersPresenter; // headers presenter for sending down notifications private DataGridCell _currentCellContainer; // Reference to the cell container corresponding to CurrentCell (use CurrentCellContainer property instead) private DataGridCell _pendingCurrentCellContainer; // Reference to the cell container that will become the current cell private 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 VirtualizedCellInfoCollection _pendingSelectedCells; // Cells that were selected that haven't gone through SelectedCellsChanged private VirtualizedCellInfoCollection _pendingUnselectedCells; // Cells that were unselected that haven't gone through SelectedCellsChanged 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 _defaultBindingGroup; // 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 DataGridItemAttachedStorage _itemAttachedStorage = new 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 private Dictionary _editingCellAutomationValueHolders = new Dictionary (); // Holds the content of edited cells. Required for raising Automation events. private DataGridCell _focusedCell = null; // Holds the cell which has logical focus. #endregion #region Constants private const string ItemsPanelPartName = "PART_RowsPresenter"; #endregion } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu
This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- InplaceBitmapMetadataWriter.cs
- CacheModeValueSerializer.cs
- BrowserPolicyValidator.cs
- ToolTipAutomationPeer.cs
- PassportAuthenticationEventArgs.cs
- BrowserTree.cs
- DisplayNameAttribute.cs
- CompressionTransform.cs
- StorageConditionPropertyMapping.cs
- DataGridViewAccessibleObject.cs
- WebPartCatalogAddVerb.cs
- FormViewPageEventArgs.cs
- XmlSchemaType.cs
- SelectedDatesCollection.cs
- NativeRecognizer.cs
- TextInfo.cs
- FontConverter.cs
- ReadOnlyMetadataCollection.cs
- WriterOutput.cs
- EntityDataSourceSelectingEventArgs.cs
- EncryptedPackageFilter.cs
- SystemKeyConverter.cs
- templategroup.cs
- HwndSourceParameters.cs
- FrameworkObject.cs
- Point3DAnimationUsingKeyFrames.cs
- ProtocolImporter.cs
- ActivityPreviewDesigner.cs
- TimeSpan.cs
- altserialization.cs
- SecurityPermission.cs
- InvalidDataException.cs
- COM2PictureConverter.cs
- FileVersion.cs
- HttpProxyTransportBindingElement.cs
- PropertyGridEditorPart.cs
- Pen.cs
- AsyncPostBackTrigger.cs
- ZipFileInfo.cs
- InternalConfigRoot.cs
- SQLConvert.cs
- EntityDataSourceChangingEventArgs.cs
- EqualityArray.cs
- Int32Rect.cs
- DeobfuscatingStream.cs
- XmlElementAttribute.cs
- DeviceContexts.cs
- SafeNativeMethodsOther.cs
- ConsoleCancelEventArgs.cs
- OutOfMemoryException.cs
- MultiPartWriter.cs
- EventLogQuery.cs
- TextDecorationCollection.cs
- TypeElementCollection.cs
- ArglessEventHandlerProxy.cs
- XmlNavigatorFilter.cs
- SignatureToken.cs
- ServiceOperationParameter.cs
- SoapObjectReader.cs
- SettingsBase.cs
- XamlVector3DCollectionSerializer.cs
- RectAnimation.cs
- GPRECT.cs
- SettingsPropertyCollection.cs
- EqualityArray.cs
- SendSecurityHeaderElementContainer.cs
- BinaryMessageFormatter.cs
- InboundActivityHelper.cs
- CellParaClient.cs
- DataObjectSettingDataEventArgs.cs
- ThemeDirectoryCompiler.cs
- TextEditorSpelling.cs
- ChangeToolStripParentVerb.cs
- ImageMetadata.cs
- FileLevelControlBuilderAttribute.cs
- DirectionalLight.cs
- CompoundFileStorageReference.cs
- ReadOnlyCollectionBuilder.cs
- NavigatorOutput.cs
- SymbolMethod.cs
- TemplateContainer.cs
- MinimizableAttributeTypeConverter.cs
- DefaultBinder.cs
- HierarchicalDataTemplate.cs
- Activator.cs
- XmlSerializerVersionAttribute.cs
- CompilerLocalReference.cs
- StyleModeStack.cs
- SrgsText.cs
- TimeoutException.cs
- XPathSingletonIterator.cs
- Assert.cs
- CodeArgumentReferenceExpression.cs
- CFStream.cs
- SQLByteStorage.cs
- IncrementalReadDecoders.cs
- GAC.cs
- XmlArrayItemAttributes.cs
- ItemsPresenter.cs
- OutputScope.cs