// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using Avalonia.Collections;
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.ComponentModel.DataAnnotations;
using Avalonia.Utilities;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls
{
internal class DataGridDataConnection
{
private int _backupSlotForCurrentChanged;
private int _columnForCurrentChanged;
private PropertyInfo[] _dataProperties;
private IEnumerable _dataSource;
private Type _dataType;
private bool _expectingCurrentChanged;
private object _itemToSelectOnCurrentChanged;
private DataGrid _owner;
private bool _scrollForCurrentChanged;
private DataGridSelectionAction _selectionActionForCurrentChanged;
public DataGridDataConnection(DataGrid owner)
{
_owner = owner;
}
public bool AllowEdit
{
get
{
if (List == null)
{
return true;
}
else
{
return !List.IsReadOnly;
}
}
}
///
/// True if the collection view says it can sort.
///
public bool AllowSort
{
get
{
if (CollectionView == null ||
(EditableCollectionView != null && (EditableCollectionView.IsAddingNew || EditableCollectionView.IsEditingItem)))
{
return false;
}
else
{
return CollectionView.CanSort;
}
}
}
public bool CommittingEdit
{
get;
private set;
}
public int Count => GetCount(true);
public bool DataIsPrimitive
{
get
{
return DataTypeIsPrimitive(DataType);
}
}
public PropertyInfo[] DataProperties
{
get
{
if (_dataProperties == null)
{
UpdateDataProperties();
}
return _dataProperties;
}
}
public IEnumerable DataSource
{
get
{
return _dataSource;
}
set
{
_dataSource = value;
// Because the DataSource is changing, we need to reset our cached values for DataType and DataProperties,
// which are dependent on the current DataSource
_dataType = null;
UpdateDataProperties();
}
}
public Type DataType
{
get
{
// We need to use the raw ItemsSource as opposed to DataSource because DataSource
// may be the ItemsSource wrapped in a collection view, in which case we wouldn't
// be able to take T to be the type if we're given IEnumerable
if (_dataType == null && _owner.Items != null)
{
_dataType = _owner.Items.GetItemType();
}
return _dataType;
}
}
public bool EventsWired
{
get;
private set;
}
private bool IsGrouping
{
get
{
return (CollectionView != null)
&& CollectionView.CanGroup
&& CollectionView.IsGrouping
&& (CollectionView.GroupingDepth > 0);
}
}
public IList List
{
get
{
return DataSource as IList;
}
}
public bool ShouldAutoGenerateColumns
{
get
{
return false;
}
}
public IDataGridCollectionView CollectionView
{
get
{
return DataSource as IDataGridCollectionView;
}
}
public IDataGridEditableCollectionView EditableCollectionView
{
get
{
return DataSource as IDataGridEditableCollectionView;
}
}
public DataGridSortDescriptionCollection SortDescriptions
{
get
{
if (CollectionView != null && CollectionView.CanSort)
{
return CollectionView.SortDescriptions;
}
else
{
return null;
}
}
}
internal bool Any()
{
return GetCount(false) > 0;
}
/// When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead.
private int GetCount(bool allowSlow)
{
return DataSource switch
{
ICollection collection => collection.Count,
DataGridCollectionView cv => cv.Count,
IEnumerable enumerable when allowSlow => enumerable.Cast().Count(),
IEnumerable enumerable when !allowSlow => enumerable.Cast().Any() ? 1 : 0,
_ => 0
};
}
///
/// Puts the entity into editing mode if possible
///
/// The entity to edit
/// True if editing was started
public bool BeginEdit(object dataItem)
{
if (dataItem == null)
{
return false;
}
IDataGridEditableCollectionView editableCollectionView = EditableCollectionView;
if (editableCollectionView != null)
{
if (editableCollectionView.IsEditingItem && (dataItem == editableCollectionView.CurrentEditItem))
{
return true;
}
else
{
editableCollectionView.EditItem(dataItem);
return editableCollectionView.IsEditingItem;
}
}
if (dataItem is IEditableObject editableDataItem)
{
editableDataItem.BeginEdit();
return true;
}
return true;
}
///
/// Cancels the current entity editing and exits the editing mode.
///
/// The entity being edited
/// True if a cancellation operation was invoked.
public bool CancelEdit(object dataItem)
{
IDataGridEditableCollectionView editableCollectionView = EditableCollectionView;
if (editableCollectionView != null)
{
if (editableCollectionView.CanCancelEdit)
{
editableCollectionView.CancelEdit();
return true;
}
return false;
}
if (dataItem is IEditableObject editableDataItem)
{
editableDataItem.CancelEdit();
return true;
}
return true;
}
public static bool CanEdit(Type type)
{
Debug.Assert(type != null);
type = type.GetNonNullableType();
return
type.IsEnum
|| type == typeof(System.String)
|| type == typeof(System.Char)
|| type == typeof(System.DateTime)
|| type == typeof(System.Boolean)
|| type == typeof(System.Byte)
|| type == typeof(System.SByte)
|| type == typeof(System.Single)
|| type == typeof(System.Double)
|| type == typeof(System.Decimal)
|| type == typeof(System.Int16)
|| type == typeof(System.Int32)
|| type == typeof(System.Int64)
|| type == typeof(System.UInt16)
|| type == typeof(System.UInt32)
|| type == typeof(System.UInt64);
}
///
/// Commits the current entity editing and exits the editing mode.
///
/// The entity being edited
/// True if a commit operation was invoked.
public bool EndEdit(object dataItem)
{
IDataGridEditableCollectionView editableCollectionView = EditableCollectionView;
if (editableCollectionView != null)
{
// IEditableCollectionView.CommitEdit can potentially change currency. If it does,
// we don't want to attempt a second commit inside our CurrentChanging event handler.
_owner.NoCurrentCellChangeCount++;
CommittingEdit = true;
try
{
editableCollectionView.CommitEdit();
}
finally
{
_owner.NoCurrentCellChangeCount--;
CommittingEdit = false;
}
return true;
}
if (dataItem is IEditableObject editableDataItem)
{
editableDataItem.EndEdit();
}
return true;
}
// Assumes index >= 0, returns null if index >= Count
public object GetDataItem(int index)
{
Debug.Assert(index >= 0);
IList list = List;
if (list != null)
{
return (index < list.Count) ? list[index] : null;
}
if (DataSource is DataGridCollectionView collectionView)
{
return (index < collectionView.Count) ? collectionView.GetItemAt(index) : null;
}
IEnumerable enumerable = DataSource;
if (enumerable != null)
{
IEnumerator enumerator = enumerable.GetEnumerator();
int i = -1;
while (enumerator.MoveNext() && i < index)
{
i++;
if (i == index)
{
return enumerator.Current;
}
}
}
return null;
}
public bool GetPropertyIsReadOnly(string propertyName)
{
if (DataType != null)
{
if (!String.IsNullOrEmpty(propertyName))
{
Type propertyType = DataType;
PropertyInfo propertyInfo = null;
List propertyNames = TypeHelper.SplitPropertyPath(propertyName);
for (int i = 0; i < propertyNames.Count; i++)
{
propertyInfo = propertyType.GetPropertyOrIndexer(propertyNames[i], out object[] index);
if (propertyInfo == null || propertyType.GetIsReadOnly() || propertyInfo.GetIsReadOnly())
{
// Either the data type is read-only, the property doesn't exist, or it does exist but is read-only
return true;
}
// Check if EditableAttribute is defined on the property and if it indicates uneditable
object[] attributes = propertyInfo.GetCustomAttributes(typeof(EditableAttribute), true);
if (attributes != null && attributes.Length > 0)
{
EditableAttribute editableAttribute = attributes[0] as EditableAttribute;
Debug.Assert(editableAttribute != null);
if (!editableAttribute.AllowEdit)
{
return true;
}
}
propertyType = propertyInfo.PropertyType.GetNonNullableType();
}
return propertyInfo == null || !propertyInfo.CanWrite || !AllowEdit || !CanEdit(propertyType);
}
else if (DataType.GetIsReadOnly())
{
return true;
}
}
return !AllowEdit;
}
public int IndexOf(object dataItem)
{
IList list = List;
if (list != null)
{
return list.IndexOf(dataItem);
}
if (DataSource is DataGridCollectionView cv)
{
return cv.IndexOf(dataItem);
}
IEnumerable enumerable = DataSource;
if (enumerable != null && dataItem != null)
{
int index = 0;
foreach (object dataItemTmp in enumerable)
{
if ((dataItem == null && dataItemTmp == null) ||
dataItem.Equals(dataItemTmp))
{
return index;
}
index++;
}
}
return -1;
}
internal void ClearDataProperties()
{
_dataProperties = null;
}
///
/// Creates a collection view around the DataGrid's source. ICollectionViewFactory is
/// used if the source implements it. Otherwise a PagedCollectionView is returned.
///
/// Enumerable source for which to create a view
/// ICollectionView view over the provided source
internal static IDataGridCollectionView CreateView(IEnumerable source)
{
Debug.Assert(source != null, "source unexpectedly null");
Debug.Assert(!(source is IDataGridCollectionView), "source is an ICollectionView");
IDataGridCollectionView collectionView = null;
if (source is IDataGridCollectionViewFactory collectionViewFactory)
{
// If the source is a collection view factory, give it a chance to produce a custom collection view.
collectionView = collectionViewFactory.CreateView();
// Intentionally not catching potential exception thrown by ICollectionViewFactory.CreateView().
}
if (collectionView == null)
{
// If we still do not have a collection view, default to a PagedCollectionView.
collectionView = new DataGridCollectionView(source);
}
return collectionView;
}
internal static bool DataTypeIsPrimitive(Type dataType)
{
if (dataType != null)
{
Type type = TypeHelper.GetNonNullableType(dataType); // no-opt if dataType isn't nullable
return type.IsPrimitive || type == typeof(string) || type == typeof(DateTime) || type == typeof(Decimal);
}
else
{
return false;
}
}
internal void MoveCurrentTo(object item, int backupSlot, int columnIndex, DataGridSelectionAction action, bool scrollIntoView)
{
if (CollectionView != null)
{
_expectingCurrentChanged = true;
_columnForCurrentChanged = columnIndex;
_itemToSelectOnCurrentChanged = item;
_selectionActionForCurrentChanged = action;
_scrollForCurrentChanged = scrollIntoView;
_backupSlotForCurrentChanged = backupSlot;
CollectionView.MoveCurrentTo(item is DataGridCollectionViewGroup ? null : item);
_expectingCurrentChanged = false;
}
}
internal void UnWireEvents(IEnumerable value)
{
if (value is INotifyCollectionChanged notifyingDataSource)
{
notifyingDataSource.CollectionChanged -= NotifyingDataSource_CollectionChanged;
}
if (SortDescriptions != null)
{
SortDescriptions.CollectionChanged -= CollectionView_SortDescriptions_CollectionChanged;
}
if (CollectionView != null)
{
CollectionView.CurrentChanged -= CollectionView_CurrentChanged;
CollectionView.CurrentChanging -= CollectionView_CurrentChanging;
}
EventsWired = false;
}
internal void WireEvents(IEnumerable value)
{
if (value is INotifyCollectionChanged notifyingDataSource)
{
notifyingDataSource.CollectionChanged += NotifyingDataSource_CollectionChanged;
}
if (SortDescriptions != null)
{
SortDescriptions.CollectionChanged += CollectionView_SortDescriptions_CollectionChanged;
}
if (CollectionView != null)
{
CollectionView.CurrentChanged += CollectionView_CurrentChanged;
CollectionView.CurrentChanging += CollectionView_CurrentChanging;
}
EventsWired = true;
}
private void CollectionView_CurrentChanged(object sender, EventArgs e)
{
if (_expectingCurrentChanged)
{
// Committing Edit could cause our item to move to a group that no longer exists. In
// this case, we need to update the item.
if (_itemToSelectOnCurrentChanged is DataGridCollectionViewGroup collectionViewGroup)
{
DataGridRowGroupInfo groupInfo = _owner.RowGroupInfoFromCollectionViewGroup(collectionViewGroup);
if (groupInfo == null)
{
// Move to the next slot if the target slot isn't visible
if (!_owner.IsSlotVisible(_backupSlotForCurrentChanged))
{
_backupSlotForCurrentChanged = _owner.GetNextVisibleSlot(_backupSlotForCurrentChanged);
}
// Move to the next best slot if we've moved past all the slots. This could happen if multiple
// groups were removed.
if (_backupSlotForCurrentChanged >= _owner.SlotCount)
{
_backupSlotForCurrentChanged = _owner.GetPreviousVisibleSlot(_owner.SlotCount);
}
// Update the itemToSelect
int newCurrentPosition = -1;
_itemToSelectOnCurrentChanged = _owner.ItemFromSlot(_backupSlotForCurrentChanged, ref newCurrentPosition);
}
}
_owner.ProcessSelectionAndCurrency(
_columnForCurrentChanged,
_itemToSelectOnCurrentChanged,
_backupSlotForCurrentChanged,
_selectionActionForCurrentChanged,
_scrollForCurrentChanged);
}
else if (CollectionView != null)
{
_owner.UpdateStateOnCurrentChanged(CollectionView.CurrentItem, CollectionView.CurrentPosition);
}
}
private void CollectionView_CurrentChanging(object sender, DataGridCurrentChangingEventArgs e)
{
if (_owner.NoCurrentCellChangeCount == 0 &&
!_expectingCurrentChanged &&
!CommittingEdit &&
!_owner.CommitEdit())
{
// If CommitEdit failed, then the user has most likely input invalid data.
// We should cancel the current change if we can, otherwise we have to abort the edit.
if (e.IsCancelable)
{
e.Cancel = true;
}
else
{
_owner.CancelEdit(DataGridEditingUnit.Row, false);
}
}
}
private void CollectionView_SortDescriptions_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_owner.ColumnsItemsInternal.Count == 0)
{
return;
}
// refresh sort description
foreach (DataGridColumn column in _owner.ColumnsItemsInternal)
{
column.HeaderCell.UpdatePseudoClasses();
}
}
private void NotifyingDataSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_owner.LoadingOrUnloadingRow)
{
throw DataGridError.DataGrid.CannotChangeItemsWhenLoadingRows();
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Debug.Assert(e.NewItems != null, "Unexpected NotifyCollectionChangedAction.Add notification");
if (ShouldAutoGenerateColumns)
{
// The columns are also affected (not just rows) in this case so we need to reset everything
_owner.InitializeElements(false /*recycleRows*/);
}
else if (!IsGrouping)
{
// If we're grouping then we handle this through the CollectionViewGroup notifications
// According to WPF, Add is a single item operation
Debug.Assert(e.NewItems.Count == 1);
_owner.InsertRowAt(e.NewStartingIndex);
}
break;
case NotifyCollectionChangedAction.Remove:
IList removedItems = e.OldItems;
if (removedItems == null || e.OldStartingIndex < 0)
{
Debug.Assert(false, "Unexpected NotifyCollectionChangedAction.Remove notification");
return;
}
if (!IsGrouping)
{
// If we're grouping then we handle this through the CollectionViewGroup notifications
// According to WPF, Remove is a single item operation
foreach (object item in e.OldItems)
{
Debug.Assert(item != null);
_owner.RemoveRowAt(e.OldStartingIndex, item);
}
}
break;
case NotifyCollectionChangedAction.Replace:
throw new NotSupportedException(); //
case NotifyCollectionChangedAction.Reset:
// Did the data type change during the reset? If not, we can recycle
// the existing rows instead of having to clear them all. We still need to clear our cached
// values for DataType and DataProperties, though, because the collection has been reset.
Type previousDataType = _dataType;
_dataType = null;
if (previousDataType != DataType)
{
ClearDataProperties();
_owner.InitializeElements(false /*recycleRows*/);
}
else
{
_owner.InitializeElements(!ShouldAutoGenerateColumns /*recycleRows*/);
}
break;
}
_owner.UpdatePseudoClasses();
}
private void UpdateDataProperties()
{
Type dataType = DataType;
if (DataSource != null && dataType != null && !DataTypeIsPrimitive(dataType))
{
_dataProperties = dataType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
Debug.Assert(_dataProperties != null);
}
else
{
_dataProperties = null;
}
}
}
}