diff --git a/src/OpenRiaServices.Client/Framework/EntityCollection.cs b/src/OpenRiaServices.Client/Framework/EntityCollection.cs index d7385024..2ff61cb7 100644 --- a/src/OpenRiaServices.Client/Framework/EntityCollection.cs +++ b/src/OpenRiaServices.Client/Framework/EntityCollection.cs @@ -20,7 +20,7 @@ namespace OpenRiaServices.Client /// Represents a collection of associated Entities. /// /// The type of in the collection - public sealed class EntityCollection : IEntityCollection, IEntityCollection + public sealed class EntityCollection : IEntityCollection, IEntityCollection, IList, IReadOnlyList #if HAS_COLLECTIONVIEW , ICollectionViewFactory #endif @@ -311,7 +311,7 @@ public void Remove(TEntity entity) if (idx != -1) { - if (this.RemoveEntity(entity)) + if (this.RemoveEntity(entity, idx)) { // If the entity was removed, raise a collection changed notification. Note that the Detach call above might // have caused a dynamic removal behind the scenes resulting in the entity no longer being in the collection, @@ -368,14 +368,32 @@ private bool TryAddEntity(TEntity entity) } } - private bool RemoveEntity(TEntity entity) + private bool RemoveEntity(TEntity entity, int index) { if (this.EntitiesHashSet.Remove(entity)) { - bool isRemoved = this.Entities.Remove(entity); - Debug.Assert(isRemoved, "The entity should be present in both Entities and EntitiesHashSet"); + Debug.Assert(object.ReferenceEquals(entity, Entities[index])); + this.Entities.RemoveAt(index); return true; } + Debug.Fail("Expected item to be part of Set"); + return false; + } + + /// + /// Remove the entity if part of the collection and returns it's index through .(-1 if no removal) + /// + /// entity to remove + /// the index of the entity before removal, or -1 if not removed + private bool TryRemoveEntity(TEntity entity, out int index) + { + if (this.EntitiesHashSet.Remove(entity)) + { + index = this.Entities.IndexOf(entity); + this.Entities.RemoveAt(index); + return true; + } + index = -1; return false; } @@ -620,17 +638,14 @@ private void OnEntityAssociationUpdated(Entity entity) { // Add matching entity to our set. When adding, we use the stronger Filter to // filter out New entities - bool added = this.TryAddEntity(typedEntity); - Debug.Assert(added); - this.RaiseCollectionChangedNotification(NotifyCollectionChangedAction.Add, typedEntity, this.Entities.Count - 1); + if (this.TryAddEntity(typedEntity)) + this.RaiseCollectionChangedNotification(NotifyCollectionChangedAction.Add, typedEntity, this.Entities.Count - 1); } - else if (containsEntity && !this._entityPredicate(typedEntity)) + // The entity is in our set but is no longer a match, so we need to remove it. + // Here we use the predicate directly, since even if the entity is New if it + // no longer matches it should be removed. + else if (!this._entityPredicate(typedEntity) && this.TryRemoveEntity(typedEntity, out int idx)) { - // The entity is in our set but is no longer a match, so we need to remove it. - // Here we use the predicate directly, since even if the entity is New if it - // no longer matches it should be removed. - int idx = this.Entities.IndexOf(typedEntity); - this.RemoveEntity(typedEntity); this.RaiseCollectionChangedNotification(NotifyCollectionChangedAction.Remove, typedEntity, idx); } } @@ -648,14 +663,13 @@ private void SourceSet_CollectionChanged(object sender, NotifyCollectionChangedE if (this._parent.EntityState != EntityState.New && args.Action == NotifyCollectionChangedAction.Add) { - TEntity[] newEntities = args.NewItems.OfType().Where(this.Filter).ToArray(); - if (newEntities.Length > 0) + List newEntities = args.NewItems.OfType().Where(this.Filter).ToList(); + if (newEntities.Count > 0) { - int newStartingIdx = -1; + int newStartingIdx = this.Entities.Count; List affectedEntities = new List(); foreach (TEntity newEntity in newEntities) { - newStartingIdx = this.Entities.Count; if (this.TryAddEntity(newEntity)) { affectedEntities.Add(newEntity); @@ -664,34 +678,21 @@ private void SourceSet_CollectionChanged(object sender, NotifyCollectionChangedE if (affectedEntities.Count > 0) { -#if SILVERLIGHT - // SL doesn't support the constructor taking a list of objects - this.RaiseCollectionChangedNotification(args.Action, (TEntity)affectedEntities.Single(), newStartingIdx); -#else this.RaiseCollectionChangedNotification(args.Action, affectedEntities, newStartingIdx); -#endif } } } else if (args.Action == NotifyCollectionChangedAction.Remove) { // if the entity is in our cached collection, remove it - TEntity[] entitiesToRemove = args.OldItems.OfType().Where(p => this.EntitiesHashSet.Contains(p)).ToArray(); - if (entitiesToRemove.Length > 0) + foreach (TEntity entityToRemove in args.OldItems.OfType()) { - int oldStartingIdx = this.Entities.IndexOf(entitiesToRemove[0]); - foreach (TEntity removedEntity in entitiesToRemove) + // If entity was part of the collection and removed, raise an event + if (this.TryRemoveEntity(entityToRemove, out int idx)) { - this.RemoveEntity(removedEntity); + // Should we do a single reset event if multiple entitites are removed ?? + this.RaiseCollectionChangedNotification(args.Action, entityToRemove, idx); } - -#if SILVERLIGHT - //// REVIEW: Should we instead send out a reset event? - // SL doesn't support the constructor taking a list of objects - this.RaiseCollectionChangedNotification(args.Action, entitiesToRemove.Single(), oldStartingIdx); -#else - this.RaiseCollectionChangedNotification(args.Action, entitiesToRemove, oldStartingIdx); -#endif } } else if (args.Action == NotifyCollectionChangedAction.Reset) @@ -790,7 +791,7 @@ IEnumerable IEntityCollection.Entities { get { - return this.Cast(); + return this; } } @@ -1012,7 +1013,16 @@ void ICollectionChangedListener.OnCollectionChanged(object sender, NotifyCollect #endif #endregion - #region ICollection Members + #region ICollection, IReadOnlyList Members + + /// + /// Gets the entity at the specified index in the collection. + /// + /// **Important**: Make sure to check first to ensure the collection is initialized + /// The zero-based index of the entity to retrieve. + /// The entity located at the specified index. + public TEntity this[int index] => Entities[index]; + bool ICollection.IsReadOnly { get @@ -1021,22 +1031,30 @@ bool ICollection.IsReadOnly return IsSourceExternal; } } + void ICollection.CopyTo(TEntity[] array, int arrayIndex) { this.Load(); this.Entities.CopyTo(array, arrayIndex); } + bool ICollection.Contains(TEntity item) { this.Load(); return this.EntitiesHashSet.Contains(item); } + bool ICollection.Remove(TEntity item) { - bool removed = this.EntitiesHashSet.Contains(item); - Remove(item); - return removed; + this.Load(); + if (this.EntitiesHashSet.Contains(item)) + { + Remove(item); + return true; + } + return false; } + /// /// Removes all items. /// @@ -1046,6 +1064,77 @@ void ICollection.Clear() foreach (var item in this.Entities.ToList()) Remove(item); } + + #endregion + + #region IList, ICollection + bool IList.IsFixedSize => this.IsSourceExternal; + + bool IList.IsReadOnly => this.IsSourceExternal; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => ((ICollection)Entities).SyncRoot; + + /// + object IList.this[int index] + { + get => this[index]; + set => throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, Resource.IsNotSupported, "Index setter")); + } + + int IList.Add(object value) + { + int countBefore = this.Count; + Add((TEntity)value); + + if (this.Count == countBefore + 1) + return countBefore; + else if (this.Count == countBefore) + return -1; + else + return Entities.IndexOf((TEntity)value, countBefore); + } + + void IList.Clear() + { + ((ICollection)this).Clear(); + } + + bool IList.Contains(object value) + { + return value is TEntity entity && ((ICollection)this).Contains(entity); + } + + int IList.IndexOf(object value) + { + if (value is not TEntity entity) + return -1; + + Load(); + return Entities.IndexOf(entity); + } + + void IList.Insert(int index, object value) + { + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, Resource.IsNotSupported, "Insert")); + } + + void IList.Remove(object value) + { + Remove((TEntity)value); + } + + void IList.RemoveAt(int index) + { + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, Resource.IsNotSupported, "RemoveAt")); + } + + void ICollection.CopyTo(Array array, int index) + { + this.Load(); + ((ICollection)Entities).CopyTo(array, index); + } #endregion } } diff --git a/src/OpenRiaServices.Client/Framework/EntitySet.cs b/src/OpenRiaServices.Client/Framework/EntitySet.cs index 5e04b18e..0390cc9d 100644 --- a/src/OpenRiaServices.Client/Framework/EntitySet.cs +++ b/src/OpenRiaServices.Client/Framework/EntitySet.cs @@ -18,7 +18,7 @@ namespace OpenRiaServices.Client /// /// Represents a collection of instances. /// - public abstract class EntitySet : IEnumerable, ICollection, INotifyCollectionChanged, IRevertibleChangeTracking, INotifyPropertyChanged + public abstract class EntitySet : IEnumerable, ICollection, IList, INotifyCollectionChanged, IRevertibleChangeTracking, INotifyPropertyChanged { private readonly Dictionary> _associationUpdateCallbackMap = new(); private readonly Type _entityType; @@ -481,7 +481,10 @@ public void Remove(Entity entity) this.OnCollectionChanged(NotifyCollectionChangedAction.Remove, entity, idx); } - internal bool Contains(Entity entity) + /// Determines whether the contains the specified entity. + /// The element to locate in the object. + /// true if the object contains the specified element; otherwise, false. + public bool Contains(Entity entity) { return this._set.Contains(entity); } @@ -940,6 +943,65 @@ void ICollection.CopyTo(Array array, int index) } #endregion + #region IList + bool IList.IsFixedSize => (_supportedOperations & (EntitySetOperations.Remove | EntitySetOperations.Add)) == 0; + + bool IList.IsReadOnly => (_supportedOperations & (EntitySetOperations.Remove | EntitySetOperations.Add)) == 0; + + object IList.this[int index] + { + get => _list[index]; + set => throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, Resource.IsNotSupported, "Index setter")); + } + + int IList.Add(object value) + { + int countBefore = Count; + Add((Entity)value); + + if (Count == countBefore + 1) + return countBefore; + else if (Count == countBefore) + return -1; + else + return List.IndexOf(value); + } + + bool IList.Contains(object value) + { + return value is Entity e && Contains(e); + } + + int IList.IndexOf(object value) + { + return _list.IndexOf(value); + } + + void IList.Insert(int index, object value) + { + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, Resource.IsNotSupported, "Insert")); + } + + void IList.Remove(object value) + { + try + { + if (value is Entity entity) + Remove(entity); + } + catch (InvalidOperationException ioe) when (ioe.Message == Resource.EntitySet_EntityNotInSet) + { + // Don't throw if item was not in the collection + } + } + + void IList.RemoveAt(int index) + { + throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, Resource.IsNotSupported, "RemoveAt")); + } + + #endregion + #region INotifyCollectionChanged Members /// @@ -1213,12 +1275,13 @@ protected override void VisitEntityRef(IEntityRef entityRef, Entity parent, Meta /// Represents a collection of instances, providing change tracking and other services. /// /// The type of this set will contain - public sealed class EntitySet : EntitySet, IEntityCollection + public sealed class EntitySet : EntitySet, IEntityCollection, IReadOnlyList #if HAS_COLLECTIONVIEW , ICollectionViewFactory #endif where TEntity : Entity { + private new List List => (List)base.List; /// /// Initializes a new instance of the EntitySet class /// @@ -1246,7 +1309,7 @@ protected override Entity CreateEntity() { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Cannot_Create_Abstract_Entity, typeof(TEntity))); } - TEntity entity = (TEntity)Activator.CreateInstance(typeof(TEntity)); + TEntity entity = Activator.CreateInstance(); return entity; } @@ -1266,7 +1329,7 @@ protected override Entity CreateEntity() /// The enumerator public new IEnumerator GetEnumerator() { - return ((IList)List).GetEnumerator(); + return List.GetEnumerator(); } /// @@ -1340,20 +1403,14 @@ protected override void OnCollectionChanged(NotifyCollectionChangedAction action base.OnCollectionChanged(action, affectedObject, index); } - #region IEnumerable Members - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - #endregion - #region ICollection Members void ICollection.CopyTo(TEntity[] array, int arrayIndex) { - ((IList)List).CopyTo(array, arrayIndex); + List.CopyTo(array, arrayIndex); } - bool ICollection.Contains(TEntity item) + /// + public bool /*ICollection.*/Contains(TEntity item) { return base.Contains(item); } @@ -1377,6 +1434,10 @@ bool ICollection.Remove(TEntity item) } #endregion + #region IReadOnlyList Members + TEntity IReadOnlyList.this[int index] => List[index]; + #endregion + #region ICollectionViewFactory #if HAS_COLLECTIONVIEW /// @@ -1414,22 +1475,7 @@ internal ListCollectionViewProxy(EntitySet source) #region IList public int Add(object value) - { - T entity = value as T; - if (entity == null) - { - throw new ArgumentException( - string.Format(CultureInfo.CurrentCulture, Resource.MustBeAnEntity, "value"), - nameof(value)); - } - - int countBefore = this.Source.Count; - this.Source.Add(entity); - - return this.Source.Count == countBefore + 1 - ? countBefore - : ((List)this.Source.List).IndexOf(entity, countBefore); - } + => ((IList)Source).Add(value); public void Clear() { @@ -1443,24 +1489,15 @@ public bool Contains(object value) public int IndexOf(object value) { - return this.Source.List.IndexOf(value); + return ((IList)this.Source.List).IndexOf(value); } public void Insert(int index, object value) - { - throw new NotSupportedException( - string.Format(CultureInfo.CurrentCulture, Resource.IsNotSupported, "Insert")); - } + => ((IList)Source).Insert(index, value); - public bool IsFixedSize - { - get { return !(this.Source.CanAdd || this.Source.CanRemove); } - } + public bool IsFixedSize => ((IList)Source).IsFixedSize; - public bool IsReadOnly - { - get { return !(this.Source.CanAdd || this.Source.CanRemove); } - } + public bool IsReadOnly => ((IList)Source).IsReadOnly; public void Remove(object value) { @@ -1472,7 +1509,7 @@ public void Remove(object value) public void RemoveAt(int index) { - this.Remove(this[index]); + Source.Remove(Source.List[index]); } public object this[int index] @@ -1490,7 +1527,7 @@ public object this[int index] public void CopyTo(Array array, int index) { - this.Source.List.CopyTo(array, index); + ((IList)this.Source.List).CopyTo(array, index); } public int Count diff --git a/src/OpenRiaServices.Client/Framework/LoadResult.cs b/src/OpenRiaServices.Client/Framework/LoadResult.cs index 6fa67e44..5857454c 100644 --- a/src/OpenRiaServices.Client/Framework/LoadResult.cs +++ b/src/OpenRiaServices.Client/Framework/LoadResult.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -11,9 +10,9 @@ namespace OpenRiaServices.Client /// The result of a sucessfully completed load operation /// /// The type of the entity loaded. - public class LoadResult : IReadOnlyCollection, ICollection, ILoadResult where TEntity : Entity + public class LoadResult : IReadOnlyList, ICollection, ILoadResult where TEntity : Entity { - private readonly ReadOnlyCollection _loadedEntites; + private readonly Data.ReadOnlyObservableLoaderCollection _loadedEntites; /// /// Initializes a new instance of the class. @@ -77,7 +76,14 @@ public LoadResult(EntityQuery query, LoadBehavior loadBehavior, IEnumer /// Gets the number of top level Entities loaded /// /// The number top level Entities loaded. - public int Count { get { return _loadedEntites.Count; } } + public int Count => _loadedEntites.Count; + + /// + /// Gets the entity at the specified zero-based index. + /// + /// The zero-based index of the entity to retrieve. + /// The entity located at the specified index. + public TEntity this[int index] => _loadedEntites[index]; #region ICollection, IEnumerator implementations ///