csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
699 lines
24 KiB
699 lines
24 KiB
// (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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if the collection view says it can sort.
|
|
/// </summary>
|
|
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<T>
|
|
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;
|
|
}
|
|
|
|
/// <param name="allowSlow">When "allowSlow" is false, method will not use Linq.Count() method and will return 0 or 1 instead.</param>
|
|
private int GetCount(bool allowSlow)
|
|
{
|
|
return DataSource switch
|
|
{
|
|
ICollection collection => collection.Count,
|
|
DataGridCollectionView cv => cv.Count,
|
|
IEnumerable enumerable when allowSlow => enumerable.Cast<object>().Count(),
|
|
IEnumerable enumerable when !allowSlow => enumerable.Cast<object>().Any() ? 1 : 0,
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Puts the entity into editing mode if possible
|
|
/// </summary>
|
|
/// <param name="dataItem">The entity to edit</param>
|
|
/// <returns>True if editing was started</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels the current entity editing and exits the editing mode.
|
|
/// </summary>
|
|
/// <param name="dataItem">The entity being edited</param>
|
|
/// <returns>True if a cancellation operation was invoked.</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Commits the current entity editing and exits the editing mode.
|
|
/// </summary>
|
|
/// <param name="dataItem">The entity being edited</param>
|
|
/// <returns>True if a commit operation was invoked.</returns>
|
|
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<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a collection view around the DataGrid's source. ICollectionViewFactory is
|
|
/// used if the source implements it. Otherwise a PagedCollectionView is returned.
|
|
/// </summary>
|
|
/// <param name="source">Enumerable source for which to create a view</param>
|
|
/// <returns>ICollectionView view over the provided source</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|