BindingListCollectionView.cs source code in C# .NET

Source code for the .NET framework in C#

                        

Code:

/ Dotnetfx_Vista_SP2 / Dotnetfx_Vista_SP2 / 8.0.50727.4016 / DEVDIV / depot / DevDiv / releases / Orcas / QFE / wpf / src / Framework / System / Windows / Data / BindingListCollectionView.cs / 1 / BindingListCollectionView.cs

                            //---------------------------------------------------------------------------- 
//
// 
//    Copyright (C) 2003 by Microsoft Corporation.  All rights reserved.
//  
//
// 
// See spec at http://avalon/connecteddata/Specs/CollectionView.mht 
//
// History: 
//  06/02/2003 : [....]   - Ported from DotNet tree
//  03/27/2004 : kenlai     - Implement IList
//
//--------------------------------------------------------------------------- 

using System; 
using System.Collections; 
using System.Collections.Generic;
using System.Collections.ObjectModel; 
using System.Collections.Specialized;
using System.Data;
using System.ComponentModel;
using System.Diagnostics; 
using System.Windows;
 
using MS.Internal; // Invariant.Assert 
using MS.Internal.Data;
using MS.Utility; 

namespace System.Windows.Data
{
    /// 
    ///  based on and associated to 
    /// and , namely ADO DataViews. 
    /// 
    public sealed class BindingListCollectionView : CollectionView, IComparer, IEditableCollectionView, IItemProperties
    { 
        //-----------------------------------------------------
        //
        //  Constructors
        // 
        //-----------------------------------------------------
 
        #region Constructors 

        ///  
        /// Constructor
        /// 
        /// Underlying IBindingList
        public BindingListCollectionView(IBindingList list) 
            : base(list)
        { 
            InternalList = list; 
            _blv = list as IBindingListView;
            _isDataView = (list is System.Data.DataView); 

            _cachedList = new ArrayList(InternalList);

            SubscribeToChanges(); 

            _group = new CollectionViewGroupRoot(this); 
            _group.GroupDescriptionChanged += new EventHandler(OnGroupDescriptionChanged); 
            ((INotifyCollectionChanged)_group).CollectionChanged += new NotifyCollectionChangedEventHandler(OnGroupChanged);
            ((INotifyCollectionChanged)_group.GroupDescriptions).CollectionChanged += new NotifyCollectionChangedEventHandler(OnGroupByChanged); 
        }

        #endregion Constructors
 

 
        //------------------------------------------------------ 
        //
        //  Public Methods 
        //
        //-----------------------------------------------------

        #region Public Methods 

        //------------------------------------------------------ 
        #region ICollectionView 

        ///  
        /// Return true if the item belongs to this view.  The item is assumed to belong to the
        /// underlying DataCollection;  this method merely takes filters into account.
        /// It is commonly used during collection-changed notifications to determine if the added/removed
        /// item requires processing. 
        /// Returns true if no filter is set on collection view.
        ///  
        public override bool PassesFilter(object item) 
        {
            if (IsCustomFilterSet) 
                return Contains(item);  // need to ask inner list, not cheap but only way to determine
            else
                return true;    // every item is contained
        } 

        ///  
        /// Return true if the item belongs to this view.  No assumptions are 
        /// made about the item. This method will behave similarly to IList.Contains().
        ///  
        public override bool Contains(object item)
        {
            VerifyRefreshNotDeferred();
 
            return (item == NewItemPlaceholder) ? (NewItemPlaceholderPosition != NewItemPlaceholderPosition.None)
                                                : CollectionProxy.Contains(item); 
        } 

        ///  
        /// Move  to the item at the given index.
        /// 
        /// Move CurrentItem to this index
        /// true if  points to an item within the view. 
        public override bool MoveCurrentToPosition(int position)
        { 
            VerifyRefreshNotDeferred(); 

            if (position < -1 || position > InternalCount) 
                throw new ArgumentOutOfRangeException("position");

            _MoveTo(position);
            return IsCurrentInView; 
        }
 
        #endregion ICollectionView 

 
        //------------------------------------------------------
        #region IComparer

        ///  Return -, 0, or +, according to whether o1 occurs before, at, or after o2 (respectively) 
        /// 
        /// first object 
        /// second object 
        /// 
        /// Compares items by their resp. index in the IList. 
        /// 
        int IComparer.Compare(object o1, object o2)
        {
            int i1 = InternalIndexOf(o1); 
            int i2 = InternalIndexOf(o2);
            return (i1 - i2); 
        } 

        #endregion IComparer 

        ///  Return the index where the given item belongs, or -1 if this index is unknown.
        /// 
        ///  
        /// If this method returns an index other than -1, it must always be true that
        /// view[index-1] < item <= view[index], where the comparisons are done via 
        /// the view's IComparer.Compare method (if any). 
        /// (This method is used by a listener's (e.g. System.Windows.Controls.ItemsControl)
        /// CollectionChanged event handler to speed up its reaction to insertion and deletion of items. 
        /// If IndexOf is  not implemented, a listener does a binary search using IComparer.Compare.)
        /// 
        /// data item
        public override int IndexOf(object item) 
        {
            VerifyRefreshNotDeferred(); 
 
            return InternalIndexOf(item);
        } 

        /// 
        /// Retrieve item at the given zero-based index in this CollectionView.
        ///  
        /// 
        /// 

The index is evaluated with any SortDescriptions or Filter being set on this CollectionView.

///
/// /// Thrown if index is out of range /// public override object GetItemAt(int index) { VerifyRefreshNotDeferred(); return InternalItemAt(index); } /// /// Implementation of IEnumerable.GetEnumerator(). /// This provides a way to enumerate the members of the collection /// without changing the currency. /// protected override IEnumerator GetEnumerator() { VerifyRefreshNotDeferred(); return InternalGetEnumerator(); } #endregion Public Methods //----------------------------------------------------- // // Public Properties // //------------------------------------------------------ #region Public Properties //----------------------------------------------------- #region ICollectionView /// /// Collection of Sort criteria to sort items in this view over the inner IBindingList. /// /// ///

/// If the underlying SourceCollection only implements IBindingList, /// then only one sort criteria in form of a /// can be added, specifying a property and direction to sort by. /// Adding more than one SortDescription will cause a InvalidOperationException. /// One such class is Generic BindingList ///

///

/// Classes like ADO's DataView (the view around a DataTable) do implement /// IBindingListView which can support sorting by more than one property /// and also filtering ///

///

/// Some IBindingList implementations do not support sorting; for those this property /// will return an empty and immutable / read-only SortDescription collection. /// Attempting to modify such a collection will cause NotSupportedException. /// Use property on this CollectionView to test if sorting is supported /// before modifying the returned collection. ///

///
public override SortDescriptionCollection SortDescriptions { get { if (InternalList.SupportsSorting) { if (_sort == null) { bool allowAdvancedSorting = _blv != null && _blv.SupportsAdvancedSorting; _sort = new BindingListSortDescriptionCollection(allowAdvancedSorting); ((INotifyCollectionChanged)_sort).CollectionChanged += new NotifyCollectionChangedEventHandler(SortDescriptionsChanged); } return _sort; } else return SortDescriptionCollection.Empty; } } /// /// Test if this ICollectionView supports sorting before adding /// to . /// /// /// ListCollectionView does implement an IComparer based sorting. /// public override bool CanSort { get { return InternalList.SupportsSorting; } } private IComparer ActiveComparer { get { return _comparer; } set { _comparer = value; _group.ActiveComparer = value; } } /// /// BindingListCollectionView does not support callback-based filtering. /// Use instead. /// public override bool CanFilter { get { return false; } } /// /// Gets or sets the filter to be used to exclude items from the collection of items returned by the data source . /// /// /// Before assigning, test if this CollectionView supports custom filtering /// . /// The actual syntax depends on the implementer of IBindingListView. ADO's DataView is /// a common example, see for its supported /// filter expression syntax. /// public string CustomFilter { get { return _customFilter; } set { if (!CanCustomFilter) throw new NotSupportedException(SR.Get(SRID.BindingListCannotCustomFilter)); if (IsAddingNew || IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringAddOrEdit, "CustomFilter")); _customFilter = value; RefreshOrDefer(); } } /// /// Test if this CollectionView supports custom filtering before assigning /// a filter string to . /// public bool CanCustomFilter { get { return ((_blv != null) && _blv.SupportsFiltering); } } /// /// Returns true if this view really supports grouping. /// When this returns false, the rest of the interface is ignored. /// public override bool CanGroup { get { return true; } } /// /// The description of grouping, indexed by level. /// public override ObservableCollection GroupDescriptions { get { return _group.GroupDescriptions; } } /// /// The top-level groups, constructed according to the descriptions /// given in GroupDescriptions and/or GroupBySelector. /// public override ReadOnlyObservableCollection Groups { get { return (_isGrouping) ? _group.Items : null; } } #endregion ICollectionView /// /// A delegate to select the group description as a function of the /// parent group and its level. /// [DefaultValue(null)] public GroupDescriptionSelectorCallback GroupBySelector { get { return _group.GroupBySelector; } set { if (!CanGroup) throw new NotSupportedException(); if (IsAddingNew || IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringAddOrEdit, "GroupBySelector")); _group.GroupBySelector = value; RefreshOrDefer(); } } /// /// Return the estimated number of records (or -1, meaning "don't know"). /// public override int Count { get { VerifyRefreshNotDeferred(); return InternalCount; } } /// /// Returns true if the resulting (filtered) view is emtpy. /// public override bool IsEmpty { get { return (NewItemPlaceholderPosition == NewItemPlaceholderPosition.None && CollectionProxy.Count == 0); } } /// /// Setting this to true informs the view that the list of items /// (after applying the sort and filter, if any) is already in the /// correct order for grouping. This allows the view to use a more /// efficient algorithm to build the groups. /// public bool IsDataInGroupOrder { get { return _group.IsDataInGroupOrder; } set { _group.IsDataInGroupOrder = value; } } #endregion Public Properties #region IEditableCollectionView #region Adding new items /// /// Indicates whether to include a placeholder for a new item, and if so, /// where to put it. /// public NewItemPlaceholderPosition NewItemPlaceholderPosition { get { return _newItemPlaceholderPosition; } set { VerifyRefreshNotDeferred(); if (value != _newItemPlaceholderPosition && IsAddingNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "NewItemPlaceholderPosition", "AddNew")); NotifyCollectionChangedEventArgs args = null; int oldIndex=-1, newIndex=-1; // we're adding, removing, or moving the placeholder. // Determine the appropriate events. switch (value) { case NewItemPlaceholderPosition.None: switch (_newItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: oldIndex = 0; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, NewItemPlaceholder, oldIndex); break; case NewItemPlaceholderPosition.AtEnd: oldIndex = InternalCount - 1; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, NewItemPlaceholder, oldIndex); break; } break; case NewItemPlaceholderPosition.AtBeginning: switch (_newItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: newIndex = 0; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, NewItemPlaceholder, newIndex); break; case NewItemPlaceholderPosition.AtBeginning: break; case NewItemPlaceholderPosition.AtEnd: oldIndex = InternalCount - 1; newIndex = 0; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Move, NewItemPlaceholder, newIndex, oldIndex); break; } break; case NewItemPlaceholderPosition.AtEnd: switch (_newItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: newIndex = InternalCount; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, NewItemPlaceholder, newIndex); break; case NewItemPlaceholderPosition.AtBeginning: oldIndex = 0; newIndex = InternalCount - 1; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Move, NewItemPlaceholder, newIndex, oldIndex); break; case NewItemPlaceholderPosition.AtEnd: break; } break; } // now make the change and raise the events if (args != null) { _newItemPlaceholderPosition = value; if (!_isGrouping) { base.OnCollectionChanged(null, args); } else { if (oldIndex >= 0) { int index = (oldIndex == 0) ? 0 : _group.Items.Count - 1; _group.RemoveSpecialItem(index, NewItemPlaceholder, false /*loading*/); } if (newIndex >= 0) { int index = (newIndex == 0) ? 0 : _group.Items.Count; _group.InsertSpecialItem(index, NewItemPlaceholder, false /*loading*/); } } OnPropertyChanged("NewItemPlaceholderPosition"); } } } /// /// Return true if the view supports . /// public bool CanAddNew { get { return !IsEditingItem && InternalList.AllowNew; } } /// /// Add a new item to the underlying collection. Returns the new item. /// After calling AddNew and changing the new item as desired, either /// or should be /// called to complete the transaction. /// public object AddNew() { VerifyRefreshNotDeferred(); if (IsEditingItem) { CommitEdit(); // implicitly close a previous EditItem } CommitNew(); // implicitly close a previous AddNew if (!CanAddNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedForView, "AddNew")); _newItemIndex = -2; // this is a signal that the next ItemAdded event comes from AddNew object newItem = InternalList.AddNew(); Debug.Assert(_newItemIndex != -2 && newItem == _newItem, "AddNew did not raise expected events"); MoveCurrentTo(newItem); ISupportInitialize isi = newItem as ISupportInitialize; if (isi != null) { isi.BeginInit(); } // DataView.AddNew calls BeginEdit on the new item, but other implementations // of IBL don't. Make up for them. if (!IsDataView) { IEditableObject ieo = newItem as IEditableObject; if (ieo != null) { ieo.BeginEdit(); } } return newItem; } // Calling IBL.AddNew() will raise an ItemAdded event. We handle this specially // to adjust the position of the new item in the view (it should be adjacent // to the placeholder), and cache the new item for use by the other APIs // related to AddNew. This method is called from ProcessCollectionChanged. // The index gives the adjusted position of the newItem in the view; this // differs from its position in the source collection by 1 if we've added // a placeholder at the beginning. void BeginAddNew(object newItem, int index) { Debug.Assert(_newItemIndex == -2 && _newItem == null, "unexpected call to BeginAddNew"); // remember the new item and its position in the underlying list _newItem = newItem; _newItemIndex = index; // adjust the position of the new item // (not needed when grouping, as we'll be inserting into the group structure) int position = index; if (!_isGrouping) { switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: -- _newItemIndex; position = 1; break; case NewItemPlaceholderPosition.AtEnd: position = InternalCount - 2; break; } } // raise events as if the new item appeared in the adjusted position ProcessCollectionChanged(new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, newItem, position)); } /// /// Complete the transaction started by . The new /// item remains in the collection, and the view's sort, filter, and grouping /// specifications (if any) are applied to the new item. /// public void CommitNew() { if (IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "CommitNew", "EditItem")); VerifyRefreshNotDeferred(); if (_newItem == null) return; // commit the new item ICancelAddNew ican = InternalList as ICancelAddNew; IEditableObject ieo; if (ican != null) { ican.EndNew(_newItemIndex); } else if ((ieo = _newItem as IEditableObject) != null) { ieo.EndEdit(); } // DataView raises events that cause us to update the view // correctly (including leaving AddNew mode). BindingList does not // raise these events. If they haven't happened, do the work now. if (_newItem != null) { int delta = (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) ? 1 : 0; NotifyCollectionChangedEventArgs args = ProcessCommitNew(_newItemIndex, _newItemIndex + delta); if (args != null) { base.OnCollectionChanged(InternalList, args); } } } /// /// Complete the transaction started by . The new /// item is removed from the collection. /// public void CancelNew() { if (IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "CancelNew", "EditItem")); VerifyRefreshNotDeferred(); if (_newItem == null) return; // cancel the AddNew ICancelAddNew ican = InternalList as ICancelAddNew; IEditableObject ieo; if (ican != null) { ican.CancelNew(_newItemIndex); } else if ((ieo = _newItem as IEditableObject) != null) { ieo.CancelEdit(); } // DataView raises events that cause us to update the view // correctly (including leaving AddNew mode). BindingList does not // raise these events. If they haven't happened, do the work now. if (_newItem != null) { Debug.Assert(true); } } // Common functionality used by CommitNew, CancelNew, and when the // new item is removed by Remove or Refresh. object EndAddNew(bool cancel) { object newItem = _newItem; _newItem = null; // leave "adding-new" mode IEditableObject ieo = newItem as IEditableObject; if (ieo != null) { if (cancel) { ieo.CancelEdit(); } else { ieo.EndEdit(); } } ISupportInitialize isi = newItem as ISupportInitialize; if (isi != null) { isi.EndInit(); } return newItem; } NotifyCollectionChangedEventArgs ProcessCommitNew(int fromIndex, int toIndex) { if (_isGrouping) { CommitNewForGrouping(); return null; } // CommitNew either causes the list to raise an event, or not. // In either case, leave AddNew mode and raise a Move event if needed. switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: fromIndex = 1; break; case NewItemPlaceholderPosition.AtEnd: fromIndex = InternalCount - 2; break; } object newItem = EndAddNew(false); NotifyCollectionChangedEventArgs result = null; if (fromIndex != toIndex) { result = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, newItem, toIndex, fromIndex); } return result; } void CommitNewForGrouping() { // for grouping we cannot pretend that the new item moves to a different position, // since it may actually appear in several new positions (belonging to several groups). // Instead, we remove the item from its temporary position, then add it to the groups // as if it had just been added to the underlying collection. int index; switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: default: index = _group.Items.Count - 1; break; case NewItemPlaceholderPosition.AtBeginning: index = 1; break; case NewItemPlaceholderPosition.AtEnd: index = _group.Items.Count - 2; break; } // End the AddNew transaction object newItem = EndAddNew(false); // remove item from its temporary position _group.RemoveSpecialItem(index, newItem, false /*loading*/); // add it to the groups AddItemToGroups(newItem); } /// /// Returns true if an transaction is in progress. /// public bool IsAddingNew { get { return (_newItem != null); } } /// /// When an transaction is in progress, this property /// returns the new item. Otherwise it returns null. /// public object CurrentAddItem { get { return _newItem; } } #endregion Adding new items #region Removing items /// /// Return true if the view supports and /// . /// public bool CanRemove { get { return !IsEditingItem && !IsAddingNew && InternalList.AllowRemove; } } /// /// Remove the item at the given index from the underlying collection. /// The index is interpreted with respect to the view (not with respect to /// the underlying collection). /// public void RemoveAt(int index) { if (IsEditingItem || IsAddingNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringAddOrEdit, "Remove")); VerifyRefreshNotDeferred(); // convert the index from "view-relative" to "list-relative" object item = GetItemAt(index); if (item == CollectionView.NewItemPlaceholder) throw new InvalidOperationException(SR.Get(SRID.RemovingPlaceholder)); if (_isGrouping) { index = InternalList.IndexOf(item); } else { int delta = (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) ? 1 : 0; index = index - delta; } // remove the item from the list InternalList.RemoveAt(index); } /// /// Remove the given item from the underlying collection. /// public void Remove(object item) { int index = InternalIndexOf(item); if (index >= 0) { RemoveAt(index); } } #endregion Removing items #region Transactional editing of an item /// /// Begins an editing transaction on the given item. The transaction is /// completed by calling either or /// . Any changes made to the item during /// the transaction are considered "pending", provided that the view supports /// the notion of "pending changes" for the given item. /// public void EditItem(object item) { VerifyRefreshNotDeferred(); if (item == NewItemPlaceholder) throw new ArgumentException(SR.Get(SRID.CannotEditPlaceholder), "item"); if (IsAddingNew) { if (Object.Equals(item, _newItem)) return; // EditItem(newItem) is a no-op CommitNew(); // implicitly close a previous AddNew } CommitEdit(); // implicitly close a previous EditItem transaction _editItem = item; IEditableObject ieo = item as IEditableObject; if (ieo != null) { ieo.BeginEdit(); } } /// /// Complete the transaction started by . /// The pending changes (if any) to the item are committed. /// public void CommitEdit() { if (IsAddingNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "CommitEdit", "AddNew")); VerifyRefreshNotDeferred(); if (_editItem == null) return; IEditableObject ieo = _editItem as IEditableObject; object editItem = _editItem; _editItem = null; if (ieo != null) { ieo.EndEdit(); } // editing may change the item's group names (and we can't tell whether // it really did). The best we can do is remove the item and re-insert // it. if (_isGrouping) { RemoveItemFromGroups(editItem); AddItemToGroups(editItem); return; } } /// /// Complete the transaction started by . /// The pending changes (if any) to the item are discarded. /// public void CancelEdit() { if (IsAddingNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "CancelEdit", "AddNew")); VerifyRefreshNotDeferred(); if (_editItem == null) return; IEditableObject ieo = _editItem as IEditableObject; _editItem = null; if (ieo != null) { ieo.CancelEdit(); } else throw new InvalidOperationException(SR.Get(SRID.CancelEditNotSupported)); } private void ImplicitlyCancelEdit() { IEditableObject ieo = _editItem as IEditableObject; _editItem = null; if (ieo != null) { ieo.CancelEdit(); } } /// /// Returns true if the view supports the notion of "pending changes" on the /// current edit item. This may vary, depending on the view and the particular /// item. For example, a view might return true if the current edit item /// implements , or if the view has special /// knowledge about the item that it can use to support rollback of pending /// changes. /// public bool CanCancelEdit { get { return (_editItem is IEditableObject); } } /// /// Returns true if an transaction is in progress. /// public bool IsEditingItem { get { return (_editItem != null); } } /// /// When an transaction is in progress, this property /// returns the affected item. Otherwise it returns null. /// public object CurrentEditItem { get { return _editItem; } } #endregion Transactional editing of an item #endregion IEditableCollectionView #region IItemProperties /// /// Returns information about the properties available on items in the /// underlying collection. This information may come from a schema, from /// a type descriptor, from a representative item, or from some other source /// known to the view. /// public ReadOnlyCollection ItemProperties { get { return GetItemProperties(); } } #endregion IItemProperties //----------------------------------------------------- // // Protected Methods // //----------------------------------------------------- #region Protected Methods /// /// Re-create the view over the associated IList /// /// /// Any sorting and filtering will take effect during Refresh. /// protected override void RefreshOverride() { lock(SyncRoot) { ClearChangeLog(); if (UpdatedOutsideDispatcher) { _shadowList = new ArrayList((ICollection)SourceCollection); } } object oldCurrentItem = CurrentItem; int oldCurrentPosition = IsEmpty ? 0 : CurrentPosition; bool oldIsCurrentAfterLast = IsCurrentAfterLast; bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; // force currency off the collection (gives user a chance to save dirty information) OnCurrentChanging(); // changing filter and sorting will cause the inner IBindingList(View) to // raise refresh action; ignore those until done setting filter/sort _ignoreInnerRefresh = true; // IBindingListView can support filtering if (IsCustomFilterSet) { _isFiltered = true; _blv.Filter = _customFilter; } else if (_isFiltered) { // app has cleared filter _isFiltered = false; _blv.RemoveFilter(); } if ((_sort != null) && (_sort.Count > 0) && (CollectionProxy != null) && (CollectionProxy.Count > 0)) { // convert Avalon SortDescription collection to .Net // (i.e. string property names become PropertyDescriptors) ListSortDescriptionCollection sorts = ConvertSortDescriptionCollection(_sort); if (sorts.Count > 0) { _isSorted = true; if (_blv == null) InternalList.ApplySort(sorts[0].PropertyDescriptor, sorts[0].SortDirection); else _blv.ApplySort(sorts); } ActiveComparer = new SortFieldComparer(_sort, Culture); } else if (_isSorted) { // undo any previous sorting _isSorted = false; InternalList.RemoveSort(); ActiveComparer = null; } PrepareGroups(); // reset currency if (oldIsCurrentBeforeFirst || IsEmpty) { SetCurrent(null, -1); } else if (oldIsCurrentAfterLast) { SetCurrent(null, InternalCount); } else { // oldCurrentItem may be null // if there are duplicates, use the position of the first matching item //ISSUE windows#868101 DataRowView.IndexOf(oldCurrentItem) returns wrong index, wrong current item gets restored int newPosition = InternalIndexOf(oldCurrentItem); if (newPosition < 0) { // oldCurrentItem not found: move to first item object newItem; newPosition = (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) ? 1 : 0; if (newPosition < InternalCount && (newItem = InternalItemAt(newPosition)) != NewItemPlaceholder) { SetCurrent(newItem, newPosition); } else { SetCurrent(null, -1); } } else { SetCurrent(oldCurrentItem, newPosition); } } // refresh cached list with any changes _cachedList = new ArrayList(InternalList); _ignoreInnerRefresh = false; // tell listeners everything has changed OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); OnCurrentChanged(); if (IsCurrentAfterLast != oldIsCurrentAfterLast) OnPropertyChanged(IsCurrentAfterLastPropertyName); if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) OnPropertyChanged(IsCurrentBeforeFirstPropertyName); if (oldCurrentPosition != CurrentPosition) OnPropertyChanged(CurrentPositionPropertyName); if (oldCurrentItem != CurrentItem) OnPropertyChanged(CurrentItemPropertyName); } /// /// Called by the the base class to notify derived class that /// a CollectionChange has been posted to the message queue. /// The purpose of this notification is to allow CollectionViews to /// take a snapshot of whatever information is needed at the time /// of the Post (most likely the state of the Data Collection). /// /// /// The NotifyCollectionChangedEventArgs that is added to the change log /// protected override void OnBeginChangeLogging(NotifyCollectionChangedEventArgs args) { if (_shadowList == null || args.Action == NotifyCollectionChangedAction.Reset) { _shadowList = new ArrayList((ICollection)SourceCollection); // the first change processed in ProcessChangeLog does // not need to be applied to the ShadowCollection in // ProcessChangeLog because the change will already be // reflected as a result of copying the Collection. _applyChangeToShadow = false; } } /// /// Must be implemented by the derived classes to process a single change on the /// UIContext. The UIContext will have allready been entered by now. /// /// /// The NotifyCollectionChangedEventArgs to be processed. /// protected override void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args) { bool shouldRaiseEvent = false; ValidateCollectionChangedEventArgs(args); int originalCurrentPosition = CurrentPosition; int oldCurrentPosition = CurrentPosition; object oldCurrentItem = CurrentItem; bool oldIsCurrentAfterLast = IsCurrentAfterLast; bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; bool moveCurrency = false; switch (args.Action) { case NotifyCollectionChangedAction.Add: if (_newItemIndex == -2) { // The ItemAdded event came from AddNew. BeginAddNew(args.NewItems[0], args.NewStartingIndex); } else if (_isGrouping) AddItemToGroups(args.NewItems[0]); else { AdjustCurrencyForAdd(args.NewStartingIndex); shouldRaiseEvent = true; } break; case NotifyCollectionChangedAction.Remove: if (_isGrouping) RemoveItemFromGroups(args.OldItems[0]); else { moveCurrency = AdjustCurrencyForRemove(args.OldStartingIndex); shouldRaiseEvent = true; } break; case NotifyCollectionChangedAction.Replace: if (_isGrouping) { RemoveItemFromGroups(args.OldItems[0]); AddItemToGroups(args.NewItems[0]); } else { moveCurrency = AdjustCurrencyForReplace(args.NewStartingIndex); shouldRaiseEvent = true; } break; case NotifyCollectionChangedAction.Move: if (!_isGrouping) { AdjustCurrencyForMove(args.OldStartingIndex, args.NewStartingIndex); shouldRaiseEvent = true; } break; case NotifyCollectionChangedAction.Reset: if (_isGrouping) RefreshOrDefer(); else shouldRaiseEvent = true; break; default: throw new NotSupportedException(SR.Get(SRID.UnexpectedCollectionChangeAction, args.Action)); } if (UpdatedOutsideDispatcher) { if (_applyChangeToShadow) { AdjustShadowCopy(args); } _applyChangeToShadow = true; } // remember whether scalar properties of the view have changed. // They may change again during the collection change event, so we // need to do the test before raising that event. bool afterLastHasChanged = (IsCurrentAfterLast != oldIsCurrentAfterLast); bool beforeFirstHasChanged = (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst); bool currentPositionHasChanged = (CurrentPosition != oldCurrentPosition); bool currentItemHasChanged = (CurrentItem != oldCurrentItem); // take a new snapshot of the scalar properties, so that we can detect // changes made during the collection change event oldIsCurrentAfterLast = IsCurrentAfterLast; oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; oldCurrentPosition = CurrentPosition; oldCurrentItem = CurrentItem; if (shouldRaiseEvent) { OnCollectionChanged(args); // Any scalar properties that changed don't need a further notification, // but do need a new snapshot if (IsCurrentAfterLast != oldIsCurrentAfterLast) { afterLastHasChanged = false; oldIsCurrentAfterLast = IsCurrentAfterLast; } if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) { beforeFirstHasChanged = false; oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; } if (CurrentPosition != oldCurrentPosition) { currentPositionHasChanged = false; oldCurrentPosition = CurrentPosition; } if (CurrentItem != oldCurrentItem) { currentItemHasChanged = false; oldCurrentItem = CurrentItem; } } // currency has to change after firing the deletion event, // so event handlers have the right picture if (moveCurrency) { MoveCurrencyOffDeletedElement(originalCurrentPosition); // changes to the scalar properties need notification afterLastHasChanged = afterLastHasChanged || (IsCurrentAfterLast != oldIsCurrentAfterLast); beforeFirstHasChanged = beforeFirstHasChanged || (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst); currentPositionHasChanged = currentPositionHasChanged || (CurrentPosition != oldCurrentPosition); currentItemHasChanged = currentItemHasChanged || (CurrentItem != oldCurrentItem); } // notify that the properties have changed. We may end up doing // double notification for properties that change during the collection // change event, but that's not harmful. Detecting the double change // is more trouble than it's worth. if (afterLastHasChanged) OnPropertyChanged(IsCurrentAfterLastPropertyName); if (beforeFirstHasChanged) OnPropertyChanged(IsCurrentBeforeFirstPropertyName); if (currentPositionHasChanged) OnPropertyChanged(CurrentPositionPropertyName); if (currentItemHasChanged) OnPropertyChanged(CurrentItemPropertyName); } #endregion Protected Methods //------------------------------------------------------ // // Private Properties // //----------------------------------------------------- #region Private Properties /// /// Protected accessor to private count. /// private int InternalCount { get { if (_isGrouping) return _group.ItemCount; return ((NewItemPlaceholderPosition == NewItemPlaceholderPosition.None) ? 0 : 1) + CollectionProxy.Count; } } private bool IsDataView { get { return _isDataView; } } #endregion Private Properties //------------------------------------------------------ // // Private Methods // //------------------------------------------------------ #region Private Methods /// /// Return index of item in the internal list. /// private int InternalIndexOf(object item) { if (_isGrouping) { return _group.LeafIndexOf(item); } if (item == NewItemPlaceholder) { switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: return -1; case NewItemPlaceholderPosition.AtBeginning: return 0; case NewItemPlaceholderPosition.AtEnd: return InternalCount - 1; } } else if (IsAddingNew && Object.Equals(item, _newItem)) { switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: return 1; case NewItemPlaceholderPosition.AtEnd: return InternalCount - 2; } } int index = CollectionProxy.IndexOf(item); // When you delete the last item from the list, // ADO returns a bad value. Item will be "invalid", in the // sense that it is not connected to a table. But IndexOf(item) // returns 10, even though there are only 10 entries in the list. // Looks like they're just returning item.Index without checking // anything. So we have to do the checking for them. if (index >= CollectionProxy.Count) { index = -1; } if (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning && index >= 0) { index += IsAddingNew ? 2 : 1; } return index; } /// /// Return item at the given index in the internal list. /// private object InternalItemAt(int index) { if (_isGrouping) { return _group.LeafAt(index); } switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: if (index == 0) return NewItemPlaceholder; --index; if (IsAddingNew) { if (index == 0) return _newItem; if (index <= _newItemIndex+1) -- index; } break; case NewItemPlaceholderPosition.AtEnd: if (index == InternalCount - 1) return NewItemPlaceholder; if (IsAddingNew && index == InternalCount-2) return _newItem; break; } return CollectionProxy[index]; } /// /// Return true if internal list contains the item. /// private bool InternalContains(object item) { if (item == NewItemPlaceholder) return (NewItemPlaceholderPosition != NewItemPlaceholderPosition.None); return (!_isGrouping) ? CollectionProxy.Contains(item) : (_group.LeafIndexOf(item) >= 0); } /// /// Return an enumerator for the internal list. /// private IEnumerator InternalGetEnumerator() { if (!_isGrouping) { return new PlaceholderAwareEnumerator(this, CollectionProxy.GetEnumerator(), NewItemPlaceholderPosition, _newItem); } else { return _group.GetLeafEnumerator(); } } // Adjust the ShadowCopy so that it accurately reflects the state of the // Data Collection immediately after the CollectionChangeEvent private void AdjustShadowCopy(NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: _shadowList.Insert(e.NewStartingIndex, e.NewItems[0]); break; case NotifyCollectionChangedAction.Remove: _shadowList.RemoveAt(e.OldStartingIndex); break; case NotifyCollectionChangedAction.Replace: _shadowList[e.OldStartingIndex] = e.NewItems[0]; break; case NotifyCollectionChangedAction.Move: _shadowList.RemoveAt(e.OldStartingIndex); _shadowList.Insert(e.NewStartingIndex, e.NewItems[0]); break; } } // true if CurrentPosition points to item within view private bool IsCurrentInView { get { return (0 <= CurrentPosition && CurrentPosition < InternalCount); } } // move to a given index private void _MoveTo (int proposed) { if (proposed == CurrentPosition || IsEmpty) return; object proposedCurrentItem = (0 <= proposed && proposed < InternalCount) ? GetItemAt(proposed) : null; if (proposedCurrentItem == NewItemPlaceholder) return; // ignore moves to the placeholder if (OKToChangeCurrent()) { bool oldIsCurrentAfterLast = IsCurrentAfterLast; bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; SetCurrent(proposedCurrentItem, proposed); OnCurrentChanged(); // notify that the properties have changed. if (IsCurrentAfterLast != oldIsCurrentAfterLast) OnPropertyChanged(IsCurrentAfterLastPropertyName); if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) OnPropertyChanged(IsCurrentBeforeFirstPropertyName); OnPropertyChanged(CurrentPositionPropertyName); OnPropertyChanged(CurrentItemPropertyName); } } // subscribe to change notifications private void SubscribeToChanges () { if (InternalList.SupportsChangeNotification) { InternalList.ListChanged += new ListChangedEventHandler(OnListChanged); } } // IBindingList has changed // At this point we may not have entered the UIContext, but // the call to base.OnCollectionChanged will marshall the change over private void OnListChanged(object sender, ListChangedEventArgs args) { if (_ignoreInnerRefresh && (args.ListChangedType == ListChangedType.Reset)) return; NotifyCollectionChangedEventArgs forwardedArgs = null; object item = null; int delta = _isGrouping ? 0 : (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) ? 1 : 0; int index = args.NewIndex; switch (args.ListChangedType) { case ListChangedType.ItemAdded: // Some implementations of IBindingList raise an extra ItemAdded event // when the new item (from a previous call to AddNew) is "committed". // [The IBindingList documentation suggests that all implementations // should do this, but only DataView seems to obey this rather // bizarre requirement.] We will ignore these extra events, unless // they arise from a commit that we initiated. There's // no way to detect them from the event args; we do it the same // way WinForms.DataGridView does - by comparing counts. if (InternalList.Count == _cachedList.Count) { if (IsAddingNew && index == _newItemIndex) { Debug.Assert(_newItem == InternalList[index], "unexpected item while committing AddNew"); forwardedArgs = ProcessCommitNew(index + delta, index + delta); } } else { // normal ItemAdded event item = InternalList[index]; forwardedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index + delta); _cachedList.Insert(index, item); Invariant.Assert(InternalList.Count == _cachedList.Count, "ItemAdded: Expect underlying IBL and cached list to be of same length"); if (index <= _newItemIndex) { ++ _newItemIndex; } } break; case ListChangedType.ItemDeleted: item = _cachedList[index]; _cachedList.RemoveAt(index); Invariant.Assert(InternalList.Count == _cachedList.Count, "ItemDeleted: Expect underlying IBL and cached list to be of same length"); if (index < _newItemIndex) { -- _newItemIndex; } // implicitly cancel AddNew and/or EditItem transactions if the relevant item is removed if (item == CurrentEditItem) { ImplicitlyCancelEdit(); } if (item == CurrentAddItem) { EndAddNew(true); switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.AtBeginning: index = 0; break; case NewItemPlaceholderPosition.AtEnd: index = InternalCount - 1; break; } } forwardedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index + delta); break; case ListChangedType.ItemMoved: if (IsAddingNew && args.OldIndex == _newItemIndex) { // ItemMoved applied to the new item. We assume this is the result // of committing a new item when a sort is in effect - the item // moves to its sorted position. There's no way to verify this assumption. item = _newItem; Debug.Assert(item == InternalList[index], "unexpected item while committing AddNew"); forwardedArgs = ProcessCommitNew(args.OldIndex, index + delta); } else { // normal ItemMoved event item = InternalList[index]; forwardedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, index+delta, args.OldIndex+delta); if (args.OldIndex < _newItemIndex && _newItemIndex < args.NewIndex) { -- _newItemIndex; } else if (args.NewIndex <= _newItemIndex && _newItemIndex < args.OldIndex) { ++ _newItemIndex; } } _cachedList.RemoveAt(args.OldIndex); _cachedList.Insert(args.NewIndex, item); Invariant.Assert(InternalList.Count == _cachedList.Count, "ItemMoved: Expect underlying IBL and cached list to be of same length"); break; case ListChangedType.ItemChanged: if (!_itemsRaisePropertyChanged.HasValue) { // check whether individual items raise PropertyChanged events // (DataRowView does) item = InternalList[args.NewIndex]; _itemsRaisePropertyChanged = (item is INotifyPropertyChanged); } // if items raise PropertyChanged, we can ignore ItemChanged; // otherwise, treat it like a Reset if (!_itemsRaisePropertyChanged.Value) { goto case ListChangedType.Reset; } break; case ListChangedType.Reset: // treat all other changes like Reset case ListChangedType.PropertyDescriptorAdded: case ListChangedType.PropertyDescriptorChanged: case ListChangedType.PropertyDescriptorDeleted: // implicitly cancel EditItem transactions if (IsEditingItem) { ImplicitlyCancelEdit(); } // adjust AddNew transactions, depending on whether the new item // survived the Reset if (IsAddingNew) { _newItemIndex = InternalList.IndexOf(_newItem); if (_newItemIndex < 0) { EndAddNew(true); } } RefreshOrDefer(); break; } if (forwardedArgs != null) { base.OnCollectionChanged(sender, forwardedArgs); } } // fix up CurrentPosition and CurrentItem after a collection change private void AdjustCurrencyForAdd(int index) { if (InternalCount == 1) { // added first item; set current at BeforeFirst SetCurrent(null, -1); } else if (index <= CurrentPosition) // adjust current index if insertion is earlier { int newPosition = CurrentPosition + 1; if (newPosition < InternalCount) { // CurrentItem might be out of [....] if underlying list is not INCC // or if this Add is the result of a Replace (Rem + Add) SetCurrent(GetItemAt(newPosition), newPosition); } else { SetCurrent(null, InternalCount); } } } // fix up CurrentPosition and CurrentItem after a collection change // return true if the current item was removed private bool AdjustCurrencyForRemove(int index) { bool result = (index == CurrentPosition); // adjust current index if deletion is earlier if (index < CurrentPosition) { SetCurrent(CurrentItem, CurrentPosition - 1); } return result; } // fix up CurrentPosition and CurrentItem after a collection change private void AdjustCurrencyForMove(int oldIndex, int newIndex) { if (oldIndex == CurrentPosition) { // moving the current item - currency moves with the item (bug 1942184) SetCurrent(GetItemAt(newIndex), newIndex); } else if (oldIndex < CurrentPosition && CurrentPosition <= newIndex) { // moving an item from before current position to after - // current item shifts back one position SetCurrent(CurrentItem, CurrentPosition - 1); } else if (newIndex <= CurrentPosition && CurrentPosition < oldIndex) { // moving an item from after current position to before - // current item shifts ahead one position SetCurrent(CurrentItem, CurrentPosition + 1); } // else no change necessary } // fix up CurrentPosition and CurrentItem after a collection change // return true if the current item was replaced private bool AdjustCurrencyForReplace(int index) { bool result = (index == CurrentPosition); if (result) { SetCurrent(GetItemAt(index), index); } return result; } private void MoveCurrencyOffDeletedElement(int oldCurrentPosition) { int lastPosition = InternalCount - 1; // OK if last is -1 // if position falls beyond last position, move back to last position int newPosition = (oldCurrentPosition < lastPosition) ? oldCurrentPosition : lastPosition; OnCurrentChanging(); if (newPosition < 0) SetCurrent(null, newPosition); else SetCurrent(InternalItemAt(newPosition), newPosition); OnCurrentChanged(); } private IList CollectionProxy { get { if (UpdatedOutsideDispatcher) return _shadowList; else return InternalList; } } /// /// Accessor to private _internalList field. /// private IBindingList InternalList { get { return _internalList; } set { _internalList = value; } } private bool IsCustomFilterSet { get { return ((_blv != null) && !String.IsNullOrEmpty(_customFilter)); } } // can the group name(s) for an item change after we've grouped the item? private bool CanGroupNamesChange { // There's no way we can deduce this - the app has to tell us. // If this is true, removing a grouped item is quite difficult. // We cannot rely on its group names to tell us which group we inserted // it into (they may have been different at insertion time), so we // have to do a linear search. get { return true; } } // SortDescription was added/removed, refresh CollView private void SortDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e) { if (IsAddingNew || IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringAddOrEdit, "Sorting")); RefreshOrDefer(); } // convert from Avalon SortDescriptions to the corresponding .NET collection private ListSortDescriptionCollection ConvertSortDescriptionCollection(SortDescriptionCollection sorts) { ListSortDescriptionCollection convertedSorts = null; // ITypedList itl = InternalList as ITypedList; if (itl != null) { PropertyDescriptorCollection pdc = itl.GetItemProperties(null); if ((pdc == null) || (pdc.Count == 0)) throw new ArgumentException(SR.Get(SRID.CannotDetermineSortByPropertiesForCollection)); ListSortDescription[] sortDescriptions = new ListSortDescription[sorts.Count]; for (int i = 0; i < sorts.Count; i++) { PropertyDescriptor dd = pdc.Find(sorts[i].PropertyName, true); if (dd == null) { string typeName = itl.GetListName(null); throw new ArgumentException(SR.Get(SRID.PropertyToSortByNotFoundOnType, typeName, sorts[i].PropertyName)); } ListSortDescription sd = new ListSortDescription(dd, sorts[i].Direction); sortDescriptions[i] = sd; } convertedSorts = new ListSortDescriptionCollection(sortDescriptions); } else { // cannot resolve property names to PropertyDescriptors -> return emtpy collection convertedSorts = new ListSortDescriptionCollection(new ListSortDescription[] {}); } return convertedSorts; } #region Grouping // divide the data items into groups void PrepareGroups() { // discard old groups _group.Clear(); // initialize the synthetic top level group _group.Initialize(); // if there's no grouping, there's nothing to do _isGrouping = (_group.GroupBy != null); if (!_isGrouping) return; // loop through the sorted/filtered list of items, dividing them // into groups (with special cases for placeholder and new item) if (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) { _group.InsertSpecialItem(0, NewItemPlaceholder, true /*loading*/); if (IsAddingNew) { _group.InsertSpecialItem(1, _newItem, true /*loading*/); } } for (int k=0, n=InternalList.Count; k /// Helper to raise a PropertyChanged event />). /// private void OnPropertyChanged(string propertyName) { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } #endregion Private Methods private class BindingListSortDescriptionCollection : SortDescriptionCollection { internal BindingListSortDescriptionCollection(bool allowMultipleDescriptions) { _allowMultipleDescriptions = allowMultipleDescriptions; } /// /// called by base class ObservableCollection<T> when an item is added to list; /// protected override void InsertItem(int index, SortDescription item) { if (!_allowMultipleDescriptions && (this.Count > 0)) { throw new InvalidOperationException(SR.Get(SRID.BindingListCanOnlySortByOneProperty)); } base.InsertItem(index, item); } private bool _allowMultipleDescriptions; } //----------------------------------------------------- // // Private Fields // //------------------------------------------------------ #region Private Fields private IBindingList _internalList; private CollectionViewGroupRoot _group; private bool _isGrouping; private IBindingListView _blv; private BindingListSortDescriptionCollection _sort; private ArrayList _shadowList; private bool _applyChangeToShadow = false; private bool _isSorted; private IComparer _comparer; private string _customFilter; private bool _isFiltered; private bool _ignoreInnerRefresh; private bool? _itemsRaisePropertyChanged; private bool _isDataView; private object _newItem; private object _editItem; private int _newItemIndex; // position of _newItem in the source collection private NewItemPlaceholderPosition _newItemPlaceholderPosition; // to handle ItemRemoved directly, we need to remember the items - // IBL's event args tell us the index, not the item itself private ArrayList _cachedList; #endregion Private Fields } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------------------- // // // Copyright (C) 2003 by Microsoft Corporation. All rights reserved. // // // // See spec at http://avalon/connecteddata/Specs/CollectionView.mht // // History: // 06/02/2003 : [....] - Ported from DotNet tree // 03/27/2004 : kenlai - Implement IList // //--------------------------------------------------------------------------- using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Data; using System.ComponentModel; using System.Diagnostics; using System.Windows; using MS.Internal; // Invariant.Assert using MS.Internal.Data; using MS.Utility; namespace System.Windows.Data { /// /// based on and associated to /// and , namely ADO DataViews. /// public sealed class BindingListCollectionView : CollectionView, IComparer, IEditableCollectionView, IItemProperties { //----------------------------------------------------- // // Constructors // //----------------------------------------------------- #region Constructors /// /// Constructor /// /// Underlying IBindingList public BindingListCollectionView(IBindingList list) : base(list) { InternalList = list; _blv = list as IBindingListView; _isDataView = (list is System.Data.DataView); _cachedList = new ArrayList(InternalList); SubscribeToChanges(); _group = new CollectionViewGroupRoot(this); _group.GroupDescriptionChanged += new EventHandler(OnGroupDescriptionChanged); ((INotifyCollectionChanged)_group).CollectionChanged += new NotifyCollectionChangedEventHandler(OnGroupChanged); ((INotifyCollectionChanged)_group.GroupDescriptions).CollectionChanged += new NotifyCollectionChangedEventHandler(OnGroupByChanged); } #endregion Constructors //------------------------------------------------------ // // Public Methods // //----------------------------------------------------- #region Public Methods //------------------------------------------------------ #region ICollectionView /// /// Return true if the item belongs to this view. The item is assumed to belong to the /// underlying DataCollection; this method merely takes filters into account. /// It is commonly used during collection-changed notifications to determine if the added/removed /// item requires processing. /// Returns true if no filter is set on collection view. /// public override bool PassesFilter(object item) { if (IsCustomFilterSet) return Contains(item); // need to ask inner list, not cheap but only way to determine else return true; // every item is contained } /// /// Return true if the item belongs to this view. No assumptions are /// made about the item. This method will behave similarly to IList.Contains(). /// public override bool Contains(object item) { VerifyRefreshNotDeferred(); return (item == NewItemPlaceholder) ? (NewItemPlaceholderPosition != NewItemPlaceholderPosition.None) : CollectionProxy.Contains(item); } /// /// Move to the item at the given index. /// /// Move CurrentItem to this index /// true if points to an item within the view. public override bool MoveCurrentToPosition(int position) { VerifyRefreshNotDeferred(); if (position < -1 || position > InternalCount) throw new ArgumentOutOfRangeException("position"); _MoveTo(position); return IsCurrentInView; } #endregion ICollectionView //------------------------------------------------------ #region IComparer /// Return -, 0, or +, according to whether o1 occurs before, at, or after o2 (respectively) /// /// first object /// second object /// /// Compares items by their resp. index in the IList. /// int IComparer.Compare(object o1, object o2) { int i1 = InternalIndexOf(o1); int i2 = InternalIndexOf(o2); return (i1 - i2); } #endregion IComparer /// Return the index where the given item belongs, or -1 if this index is unknown. /// /// /// If this method returns an index other than -1, it must always be true that /// view[index-1] < item <= view[index], where the comparisons are done via /// the view's IComparer.Compare method (if any). /// (This method is used by a listener's (e.g. System.Windows.Controls.ItemsControl) /// CollectionChanged event handler to speed up its reaction to insertion and deletion of items. /// If IndexOf is not implemented, a listener does a binary search using IComparer.Compare.) /// /// data item public override int IndexOf(object item) { VerifyRefreshNotDeferred(); return InternalIndexOf(item); } /// /// Retrieve item at the given zero-based index in this CollectionView. /// /// ///

The index is evaluated with any SortDescriptions or Filter being set on this CollectionView.

///
/// /// Thrown if index is out of range /// public override object GetItemAt(int index) { VerifyRefreshNotDeferred(); return InternalItemAt(index); } /// /// Implementation of IEnumerable.GetEnumerator(). /// This provides a way to enumerate the members of the collection /// without changing the currency. /// protected override IEnumerator GetEnumerator() { VerifyRefreshNotDeferred(); return InternalGetEnumerator(); } #endregion Public Methods //----------------------------------------------------- // // Public Properties // //------------------------------------------------------ #region Public Properties //----------------------------------------------------- #region ICollectionView /// /// Collection of Sort criteria to sort items in this view over the inner IBindingList. /// /// ///

/// If the underlying SourceCollection only implements IBindingList, /// then only one sort criteria in form of a /// can be added, specifying a property and direction to sort by. /// Adding more than one SortDescription will cause a InvalidOperationException. /// One such class is Generic BindingList ///

///

/// Classes like ADO's DataView (the view around a DataTable) do implement /// IBindingListView which can support sorting by more than one property /// and also filtering ///

///

/// Some IBindingList implementations do not support sorting; for those this property /// will return an empty and immutable / read-only SortDescription collection. /// Attempting to modify such a collection will cause NotSupportedException. /// Use property on this CollectionView to test if sorting is supported /// before modifying the returned collection. ///

///
public override SortDescriptionCollection SortDescriptions { get { if (InternalList.SupportsSorting) { if (_sort == null) { bool allowAdvancedSorting = _blv != null && _blv.SupportsAdvancedSorting; _sort = new BindingListSortDescriptionCollection(allowAdvancedSorting); ((INotifyCollectionChanged)_sort).CollectionChanged += new NotifyCollectionChangedEventHandler(SortDescriptionsChanged); } return _sort; } else return SortDescriptionCollection.Empty; } } /// /// Test if this ICollectionView supports sorting before adding /// to . /// /// /// ListCollectionView does implement an IComparer based sorting. /// public override bool CanSort { get { return InternalList.SupportsSorting; } } private IComparer ActiveComparer { get { return _comparer; } set { _comparer = value; _group.ActiveComparer = value; } } /// /// BindingListCollectionView does not support callback-based filtering. /// Use instead. /// public override bool CanFilter { get { return false; } } /// /// Gets or sets the filter to be used to exclude items from the collection of items returned by the data source . /// /// /// Before assigning, test if this CollectionView supports custom filtering /// . /// The actual syntax depends on the implementer of IBindingListView. ADO's DataView is /// a common example, see for its supported /// filter expression syntax. /// public string CustomFilter { get { return _customFilter; } set { if (!CanCustomFilter) throw new NotSupportedException(SR.Get(SRID.BindingListCannotCustomFilter)); if (IsAddingNew || IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringAddOrEdit, "CustomFilter")); _customFilter = value; RefreshOrDefer(); } } /// /// Test if this CollectionView supports custom filtering before assigning /// a filter string to . /// public bool CanCustomFilter { get { return ((_blv != null) && _blv.SupportsFiltering); } } /// /// Returns true if this view really supports grouping. /// When this returns false, the rest of the interface is ignored. /// public override bool CanGroup { get { return true; } } /// /// The description of grouping, indexed by level. /// public override ObservableCollection GroupDescriptions { get { return _group.GroupDescriptions; } } /// /// The top-level groups, constructed according to the descriptions /// given in GroupDescriptions and/or GroupBySelector. /// public override ReadOnlyObservableCollection Groups { get { return (_isGrouping) ? _group.Items : null; } } #endregion ICollectionView /// /// A delegate to select the group description as a function of the /// parent group and its level. /// [DefaultValue(null)] public GroupDescriptionSelectorCallback GroupBySelector { get { return _group.GroupBySelector; } set { if (!CanGroup) throw new NotSupportedException(); if (IsAddingNew || IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringAddOrEdit, "GroupBySelector")); _group.GroupBySelector = value; RefreshOrDefer(); } } /// /// Return the estimated number of records (or -1, meaning "don't know"). /// public override int Count { get { VerifyRefreshNotDeferred(); return InternalCount; } } /// /// Returns true if the resulting (filtered) view is emtpy. /// public override bool IsEmpty { get { return (NewItemPlaceholderPosition == NewItemPlaceholderPosition.None && CollectionProxy.Count == 0); } } /// /// Setting this to true informs the view that the list of items /// (after applying the sort and filter, if any) is already in the /// correct order for grouping. This allows the view to use a more /// efficient algorithm to build the groups. /// public bool IsDataInGroupOrder { get { return _group.IsDataInGroupOrder; } set { _group.IsDataInGroupOrder = value; } } #endregion Public Properties #region IEditableCollectionView #region Adding new items /// /// Indicates whether to include a placeholder for a new item, and if so, /// where to put it. /// public NewItemPlaceholderPosition NewItemPlaceholderPosition { get { return _newItemPlaceholderPosition; } set { VerifyRefreshNotDeferred(); if (value != _newItemPlaceholderPosition && IsAddingNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "NewItemPlaceholderPosition", "AddNew")); NotifyCollectionChangedEventArgs args = null; int oldIndex=-1, newIndex=-1; // we're adding, removing, or moving the placeholder. // Determine the appropriate events. switch (value) { case NewItemPlaceholderPosition.None: switch (_newItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: oldIndex = 0; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, NewItemPlaceholder, oldIndex); break; case NewItemPlaceholderPosition.AtEnd: oldIndex = InternalCount - 1; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, NewItemPlaceholder, oldIndex); break; } break; case NewItemPlaceholderPosition.AtBeginning: switch (_newItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: newIndex = 0; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, NewItemPlaceholder, newIndex); break; case NewItemPlaceholderPosition.AtBeginning: break; case NewItemPlaceholderPosition.AtEnd: oldIndex = InternalCount - 1; newIndex = 0; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Move, NewItemPlaceholder, newIndex, oldIndex); break; } break; case NewItemPlaceholderPosition.AtEnd: switch (_newItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: newIndex = InternalCount; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, NewItemPlaceholder, newIndex); break; case NewItemPlaceholderPosition.AtBeginning: oldIndex = 0; newIndex = InternalCount - 1; args = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Move, NewItemPlaceholder, newIndex, oldIndex); break; case NewItemPlaceholderPosition.AtEnd: break; } break; } // now make the change and raise the events if (args != null) { _newItemPlaceholderPosition = value; if (!_isGrouping) { base.OnCollectionChanged(null, args); } else { if (oldIndex >= 0) { int index = (oldIndex == 0) ? 0 : _group.Items.Count - 1; _group.RemoveSpecialItem(index, NewItemPlaceholder, false /*loading*/); } if (newIndex >= 0) { int index = (newIndex == 0) ? 0 : _group.Items.Count; _group.InsertSpecialItem(index, NewItemPlaceholder, false /*loading*/); } } OnPropertyChanged("NewItemPlaceholderPosition"); } } } /// /// Return true if the view supports . /// public bool CanAddNew { get { return !IsEditingItem && InternalList.AllowNew; } } /// /// Add a new item to the underlying collection. Returns the new item. /// After calling AddNew and changing the new item as desired, either /// or should be /// called to complete the transaction. /// public object AddNew() { VerifyRefreshNotDeferred(); if (IsEditingItem) { CommitEdit(); // implicitly close a previous EditItem } CommitNew(); // implicitly close a previous AddNew if (!CanAddNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedForView, "AddNew")); _newItemIndex = -2; // this is a signal that the next ItemAdded event comes from AddNew object newItem = InternalList.AddNew(); Debug.Assert(_newItemIndex != -2 && newItem == _newItem, "AddNew did not raise expected events"); MoveCurrentTo(newItem); ISupportInitialize isi = newItem as ISupportInitialize; if (isi != null) { isi.BeginInit(); } // DataView.AddNew calls BeginEdit on the new item, but other implementations // of IBL don't. Make up for them. if (!IsDataView) { IEditableObject ieo = newItem as IEditableObject; if (ieo != null) { ieo.BeginEdit(); } } return newItem; } // Calling IBL.AddNew() will raise an ItemAdded event. We handle this specially // to adjust the position of the new item in the view (it should be adjacent // to the placeholder), and cache the new item for use by the other APIs // related to AddNew. This method is called from ProcessCollectionChanged. // The index gives the adjusted position of the newItem in the view; this // differs from its position in the source collection by 1 if we've added // a placeholder at the beginning. void BeginAddNew(object newItem, int index) { Debug.Assert(_newItemIndex == -2 && _newItem == null, "unexpected call to BeginAddNew"); // remember the new item and its position in the underlying list _newItem = newItem; _newItemIndex = index; // adjust the position of the new item // (not needed when grouping, as we'll be inserting into the group structure) int position = index; if (!_isGrouping) { switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: -- _newItemIndex; position = 1; break; case NewItemPlaceholderPosition.AtEnd: position = InternalCount - 2; break; } } // raise events as if the new item appeared in the adjusted position ProcessCollectionChanged(new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, newItem, position)); } /// /// Complete the transaction started by . The new /// item remains in the collection, and the view's sort, filter, and grouping /// specifications (if any) are applied to the new item. /// public void CommitNew() { if (IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "CommitNew", "EditItem")); VerifyRefreshNotDeferred(); if (_newItem == null) return; // commit the new item ICancelAddNew ican = InternalList as ICancelAddNew; IEditableObject ieo; if (ican != null) { ican.EndNew(_newItemIndex); } else if ((ieo = _newItem as IEditableObject) != null) { ieo.EndEdit(); } // DataView raises events that cause us to update the view // correctly (including leaving AddNew mode). BindingList does not // raise these events. If they haven't happened, do the work now. if (_newItem != null) { int delta = (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) ? 1 : 0; NotifyCollectionChangedEventArgs args = ProcessCommitNew(_newItemIndex, _newItemIndex + delta); if (args != null) { base.OnCollectionChanged(InternalList, args); } } } /// /// Complete the transaction started by . The new /// item is removed from the collection. /// public void CancelNew() { if (IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "CancelNew", "EditItem")); VerifyRefreshNotDeferred(); if (_newItem == null) return; // cancel the AddNew ICancelAddNew ican = InternalList as ICancelAddNew; IEditableObject ieo; if (ican != null) { ican.CancelNew(_newItemIndex); } else if ((ieo = _newItem as IEditableObject) != null) { ieo.CancelEdit(); } // DataView raises events that cause us to update the view // correctly (including leaving AddNew mode). BindingList does not // raise these events. If they haven't happened, do the work now. if (_newItem != null) { Debug.Assert(true); } } // Common functionality used by CommitNew, CancelNew, and when the // new item is removed by Remove or Refresh. object EndAddNew(bool cancel) { object newItem = _newItem; _newItem = null; // leave "adding-new" mode IEditableObject ieo = newItem as IEditableObject; if (ieo != null) { if (cancel) { ieo.CancelEdit(); } else { ieo.EndEdit(); } } ISupportInitialize isi = newItem as ISupportInitialize; if (isi != null) { isi.EndInit(); } return newItem; } NotifyCollectionChangedEventArgs ProcessCommitNew(int fromIndex, int toIndex) { if (_isGrouping) { CommitNewForGrouping(); return null; } // CommitNew either causes the list to raise an event, or not. // In either case, leave AddNew mode and raise a Move event if needed. switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: fromIndex = 1; break; case NewItemPlaceholderPosition.AtEnd: fromIndex = InternalCount - 2; break; } object newItem = EndAddNew(false); NotifyCollectionChangedEventArgs result = null; if (fromIndex != toIndex) { result = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, newItem, toIndex, fromIndex); } return result; } void CommitNewForGrouping() { // for grouping we cannot pretend that the new item moves to a different position, // since it may actually appear in several new positions (belonging to several groups). // Instead, we remove the item from its temporary position, then add it to the groups // as if it had just been added to the underlying collection. int index; switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: default: index = _group.Items.Count - 1; break; case NewItemPlaceholderPosition.AtBeginning: index = 1; break; case NewItemPlaceholderPosition.AtEnd: index = _group.Items.Count - 2; break; } // End the AddNew transaction object newItem = EndAddNew(false); // remove item from its temporary position _group.RemoveSpecialItem(index, newItem, false /*loading*/); // add it to the groups AddItemToGroups(newItem); } /// /// Returns true if an transaction is in progress. /// public bool IsAddingNew { get { return (_newItem != null); } } /// /// When an transaction is in progress, this property /// returns the new item. Otherwise it returns null. /// public object CurrentAddItem { get { return _newItem; } } #endregion Adding new items #region Removing items /// /// Return true if the view supports and /// . /// public bool CanRemove { get { return !IsEditingItem && !IsAddingNew && InternalList.AllowRemove; } } /// /// Remove the item at the given index from the underlying collection. /// The index is interpreted with respect to the view (not with respect to /// the underlying collection). /// public void RemoveAt(int index) { if (IsEditingItem || IsAddingNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringAddOrEdit, "Remove")); VerifyRefreshNotDeferred(); // convert the index from "view-relative" to "list-relative" object item = GetItemAt(index); if (item == CollectionView.NewItemPlaceholder) throw new InvalidOperationException(SR.Get(SRID.RemovingPlaceholder)); if (_isGrouping) { index = InternalList.IndexOf(item); } else { int delta = (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) ? 1 : 0; index = index - delta; } // remove the item from the list InternalList.RemoveAt(index); } /// /// Remove the given item from the underlying collection. /// public void Remove(object item) { int index = InternalIndexOf(item); if (index >= 0) { RemoveAt(index); } } #endregion Removing items #region Transactional editing of an item /// /// Begins an editing transaction on the given item. The transaction is /// completed by calling either or /// . Any changes made to the item during /// the transaction are considered "pending", provided that the view supports /// the notion of "pending changes" for the given item. /// public void EditItem(object item) { VerifyRefreshNotDeferred(); if (item == NewItemPlaceholder) throw new ArgumentException(SR.Get(SRID.CannotEditPlaceholder), "item"); if (IsAddingNew) { if (Object.Equals(item, _newItem)) return; // EditItem(newItem) is a no-op CommitNew(); // implicitly close a previous AddNew } CommitEdit(); // implicitly close a previous EditItem transaction _editItem = item; IEditableObject ieo = item as IEditableObject; if (ieo != null) { ieo.BeginEdit(); } } /// /// Complete the transaction started by . /// The pending changes (if any) to the item are committed. /// public void CommitEdit() { if (IsAddingNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "CommitEdit", "AddNew")); VerifyRefreshNotDeferred(); if (_editItem == null) return; IEditableObject ieo = _editItem as IEditableObject; object editItem = _editItem; _editItem = null; if (ieo != null) { ieo.EndEdit(); } // editing may change the item's group names (and we can't tell whether // it really did). The best we can do is remove the item and re-insert // it. if (_isGrouping) { RemoveItemFromGroups(editItem); AddItemToGroups(editItem); return; } } /// /// Complete the transaction started by . /// The pending changes (if any) to the item are discarded. /// public void CancelEdit() { if (IsAddingNew) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringTransaction, "CancelEdit", "AddNew")); VerifyRefreshNotDeferred(); if (_editItem == null) return; IEditableObject ieo = _editItem as IEditableObject; _editItem = null; if (ieo != null) { ieo.CancelEdit(); } else throw new InvalidOperationException(SR.Get(SRID.CancelEditNotSupported)); } private void ImplicitlyCancelEdit() { IEditableObject ieo = _editItem as IEditableObject; _editItem = null; if (ieo != null) { ieo.CancelEdit(); } } /// /// Returns true if the view supports the notion of "pending changes" on the /// current edit item. This may vary, depending on the view and the particular /// item. For example, a view might return true if the current edit item /// implements , or if the view has special /// knowledge about the item that it can use to support rollback of pending /// changes. /// public bool CanCancelEdit { get { return (_editItem is IEditableObject); } } /// /// Returns true if an transaction is in progress. /// public bool IsEditingItem { get { return (_editItem != null); } } /// /// When an transaction is in progress, this property /// returns the affected item. Otherwise it returns null. /// public object CurrentEditItem { get { return _editItem; } } #endregion Transactional editing of an item #endregion IEditableCollectionView #region IItemProperties /// /// Returns information about the properties available on items in the /// underlying collection. This information may come from a schema, from /// a type descriptor, from a representative item, or from some other source /// known to the view. /// public ReadOnlyCollection ItemProperties { get { return GetItemProperties(); } } #endregion IItemProperties //----------------------------------------------------- // // Protected Methods // //----------------------------------------------------- #region Protected Methods /// /// Re-create the view over the associated IList /// /// /// Any sorting and filtering will take effect during Refresh. /// protected override void RefreshOverride() { lock(SyncRoot) { ClearChangeLog(); if (UpdatedOutsideDispatcher) { _shadowList = new ArrayList((ICollection)SourceCollection); } } object oldCurrentItem = CurrentItem; int oldCurrentPosition = IsEmpty ? 0 : CurrentPosition; bool oldIsCurrentAfterLast = IsCurrentAfterLast; bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; // force currency off the collection (gives user a chance to save dirty information) OnCurrentChanging(); // changing filter and sorting will cause the inner IBindingList(View) to // raise refresh action; ignore those until done setting filter/sort _ignoreInnerRefresh = true; // IBindingListView can support filtering if (IsCustomFilterSet) { _isFiltered = true; _blv.Filter = _customFilter; } else if (_isFiltered) { // app has cleared filter _isFiltered = false; _blv.RemoveFilter(); } if ((_sort != null) && (_sort.Count > 0) && (CollectionProxy != null) && (CollectionProxy.Count > 0)) { // convert Avalon SortDescription collection to .Net // (i.e. string property names become PropertyDescriptors) ListSortDescriptionCollection sorts = ConvertSortDescriptionCollection(_sort); if (sorts.Count > 0) { _isSorted = true; if (_blv == null) InternalList.ApplySort(sorts[0].PropertyDescriptor, sorts[0].SortDirection); else _blv.ApplySort(sorts); } ActiveComparer = new SortFieldComparer(_sort, Culture); } else if (_isSorted) { // undo any previous sorting _isSorted = false; InternalList.RemoveSort(); ActiveComparer = null; } PrepareGroups(); // reset currency if (oldIsCurrentBeforeFirst || IsEmpty) { SetCurrent(null, -1); } else if (oldIsCurrentAfterLast) { SetCurrent(null, InternalCount); } else { // oldCurrentItem may be null // if there are duplicates, use the position of the first matching item //ISSUE windows#868101 DataRowView.IndexOf(oldCurrentItem) returns wrong index, wrong current item gets restored int newPosition = InternalIndexOf(oldCurrentItem); if (newPosition < 0) { // oldCurrentItem not found: move to first item object newItem; newPosition = (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) ? 1 : 0; if (newPosition < InternalCount && (newItem = InternalItemAt(newPosition)) != NewItemPlaceholder) { SetCurrent(newItem, newPosition); } else { SetCurrent(null, -1); } } else { SetCurrent(oldCurrentItem, newPosition); } } // refresh cached list with any changes _cachedList = new ArrayList(InternalList); _ignoreInnerRefresh = false; // tell listeners everything has changed OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); OnCurrentChanged(); if (IsCurrentAfterLast != oldIsCurrentAfterLast) OnPropertyChanged(IsCurrentAfterLastPropertyName); if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) OnPropertyChanged(IsCurrentBeforeFirstPropertyName); if (oldCurrentPosition != CurrentPosition) OnPropertyChanged(CurrentPositionPropertyName); if (oldCurrentItem != CurrentItem) OnPropertyChanged(CurrentItemPropertyName); } /// /// Called by the the base class to notify derived class that /// a CollectionChange has been posted to the message queue. /// The purpose of this notification is to allow CollectionViews to /// take a snapshot of whatever information is needed at the time /// of the Post (most likely the state of the Data Collection). /// /// /// The NotifyCollectionChangedEventArgs that is added to the change log /// protected override void OnBeginChangeLogging(NotifyCollectionChangedEventArgs args) { if (_shadowList == null || args.Action == NotifyCollectionChangedAction.Reset) { _shadowList = new ArrayList((ICollection)SourceCollection); // the first change processed in ProcessChangeLog does // not need to be applied to the ShadowCollection in // ProcessChangeLog because the change will already be // reflected as a result of copying the Collection. _applyChangeToShadow = false; } } /// /// Must be implemented by the derived classes to process a single change on the /// UIContext. The UIContext will have allready been entered by now. /// /// /// The NotifyCollectionChangedEventArgs to be processed. /// protected override void ProcessCollectionChanged(NotifyCollectionChangedEventArgs args) { bool shouldRaiseEvent = false; ValidateCollectionChangedEventArgs(args); int originalCurrentPosition = CurrentPosition; int oldCurrentPosition = CurrentPosition; object oldCurrentItem = CurrentItem; bool oldIsCurrentAfterLast = IsCurrentAfterLast; bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; bool moveCurrency = false; switch (args.Action) { case NotifyCollectionChangedAction.Add: if (_newItemIndex == -2) { // The ItemAdded event came from AddNew. BeginAddNew(args.NewItems[0], args.NewStartingIndex); } else if (_isGrouping) AddItemToGroups(args.NewItems[0]); else { AdjustCurrencyForAdd(args.NewStartingIndex); shouldRaiseEvent = true; } break; case NotifyCollectionChangedAction.Remove: if (_isGrouping) RemoveItemFromGroups(args.OldItems[0]); else { moveCurrency = AdjustCurrencyForRemove(args.OldStartingIndex); shouldRaiseEvent = true; } break; case NotifyCollectionChangedAction.Replace: if (_isGrouping) { RemoveItemFromGroups(args.OldItems[0]); AddItemToGroups(args.NewItems[0]); } else { moveCurrency = AdjustCurrencyForReplace(args.NewStartingIndex); shouldRaiseEvent = true; } break; case NotifyCollectionChangedAction.Move: if (!_isGrouping) { AdjustCurrencyForMove(args.OldStartingIndex, args.NewStartingIndex); shouldRaiseEvent = true; } break; case NotifyCollectionChangedAction.Reset: if (_isGrouping) RefreshOrDefer(); else shouldRaiseEvent = true; break; default: throw new NotSupportedException(SR.Get(SRID.UnexpectedCollectionChangeAction, args.Action)); } if (UpdatedOutsideDispatcher) { if (_applyChangeToShadow) { AdjustShadowCopy(args); } _applyChangeToShadow = true; } // remember whether scalar properties of the view have changed. // They may change again during the collection change event, so we // need to do the test before raising that event. bool afterLastHasChanged = (IsCurrentAfterLast != oldIsCurrentAfterLast); bool beforeFirstHasChanged = (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst); bool currentPositionHasChanged = (CurrentPosition != oldCurrentPosition); bool currentItemHasChanged = (CurrentItem != oldCurrentItem); // take a new snapshot of the scalar properties, so that we can detect // changes made during the collection change event oldIsCurrentAfterLast = IsCurrentAfterLast; oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; oldCurrentPosition = CurrentPosition; oldCurrentItem = CurrentItem; if (shouldRaiseEvent) { OnCollectionChanged(args); // Any scalar properties that changed don't need a further notification, // but do need a new snapshot if (IsCurrentAfterLast != oldIsCurrentAfterLast) { afterLastHasChanged = false; oldIsCurrentAfterLast = IsCurrentAfterLast; } if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) { beforeFirstHasChanged = false; oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; } if (CurrentPosition != oldCurrentPosition) { currentPositionHasChanged = false; oldCurrentPosition = CurrentPosition; } if (CurrentItem != oldCurrentItem) { currentItemHasChanged = false; oldCurrentItem = CurrentItem; } } // currency has to change after firing the deletion event, // so event handlers have the right picture if (moveCurrency) { MoveCurrencyOffDeletedElement(originalCurrentPosition); // changes to the scalar properties need notification afterLastHasChanged = afterLastHasChanged || (IsCurrentAfterLast != oldIsCurrentAfterLast); beforeFirstHasChanged = beforeFirstHasChanged || (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst); currentPositionHasChanged = currentPositionHasChanged || (CurrentPosition != oldCurrentPosition); currentItemHasChanged = currentItemHasChanged || (CurrentItem != oldCurrentItem); } // notify that the properties have changed. We may end up doing // double notification for properties that change during the collection // change event, but that's not harmful. Detecting the double change // is more trouble than it's worth. if (afterLastHasChanged) OnPropertyChanged(IsCurrentAfterLastPropertyName); if (beforeFirstHasChanged) OnPropertyChanged(IsCurrentBeforeFirstPropertyName); if (currentPositionHasChanged) OnPropertyChanged(CurrentPositionPropertyName); if (currentItemHasChanged) OnPropertyChanged(CurrentItemPropertyName); } #endregion Protected Methods //------------------------------------------------------ // // Private Properties // //----------------------------------------------------- #region Private Properties /// /// Protected accessor to private count. /// private int InternalCount { get { if (_isGrouping) return _group.ItemCount; return ((NewItemPlaceholderPosition == NewItemPlaceholderPosition.None) ? 0 : 1) + CollectionProxy.Count; } } private bool IsDataView { get { return _isDataView; } } #endregion Private Properties //------------------------------------------------------ // // Private Methods // //------------------------------------------------------ #region Private Methods /// /// Return index of item in the internal list. /// private int InternalIndexOf(object item) { if (_isGrouping) { return _group.LeafIndexOf(item); } if (item == NewItemPlaceholder) { switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: return -1; case NewItemPlaceholderPosition.AtBeginning: return 0; case NewItemPlaceholderPosition.AtEnd: return InternalCount - 1; } } else if (IsAddingNew && Object.Equals(item, _newItem)) { switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: return 1; case NewItemPlaceholderPosition.AtEnd: return InternalCount - 2; } } int index = CollectionProxy.IndexOf(item); // When you delete the last item from the list, // ADO returns a bad value. Item will be "invalid", in the // sense that it is not connected to a table. But IndexOf(item) // returns 10, even though there are only 10 entries in the list. // Looks like they're just returning item.Index without checking // anything. So we have to do the checking for them. if (index >= CollectionProxy.Count) { index = -1; } if (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning && index >= 0) { index += IsAddingNew ? 2 : 1; } return index; } /// /// Return item at the given index in the internal list. /// private object InternalItemAt(int index) { if (_isGrouping) { return _group.LeafAt(index); } switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.None: break; case NewItemPlaceholderPosition.AtBeginning: if (index == 0) return NewItemPlaceholder; --index; if (IsAddingNew) { if (index == 0) return _newItem; if (index <= _newItemIndex+1) -- index; } break; case NewItemPlaceholderPosition.AtEnd: if (index == InternalCount - 1) return NewItemPlaceholder; if (IsAddingNew && index == InternalCount-2) return _newItem; break; } return CollectionProxy[index]; } /// /// Return true if internal list contains the item. /// private bool InternalContains(object item) { if (item == NewItemPlaceholder) return (NewItemPlaceholderPosition != NewItemPlaceholderPosition.None); return (!_isGrouping) ? CollectionProxy.Contains(item) : (_group.LeafIndexOf(item) >= 0); } /// /// Return an enumerator for the internal list. /// private IEnumerator InternalGetEnumerator() { if (!_isGrouping) { return new PlaceholderAwareEnumerator(this, CollectionProxy.GetEnumerator(), NewItemPlaceholderPosition, _newItem); } else { return _group.GetLeafEnumerator(); } } // Adjust the ShadowCopy so that it accurately reflects the state of the // Data Collection immediately after the CollectionChangeEvent private void AdjustShadowCopy(NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: _shadowList.Insert(e.NewStartingIndex, e.NewItems[0]); break; case NotifyCollectionChangedAction.Remove: _shadowList.RemoveAt(e.OldStartingIndex); break; case NotifyCollectionChangedAction.Replace: _shadowList[e.OldStartingIndex] = e.NewItems[0]; break; case NotifyCollectionChangedAction.Move: _shadowList.RemoveAt(e.OldStartingIndex); _shadowList.Insert(e.NewStartingIndex, e.NewItems[0]); break; } } // true if CurrentPosition points to item within view private bool IsCurrentInView { get { return (0 <= CurrentPosition && CurrentPosition < InternalCount); } } // move to a given index private void _MoveTo (int proposed) { if (proposed == CurrentPosition || IsEmpty) return; object proposedCurrentItem = (0 <= proposed && proposed < InternalCount) ? GetItemAt(proposed) : null; if (proposedCurrentItem == NewItemPlaceholder) return; // ignore moves to the placeholder if (OKToChangeCurrent()) { bool oldIsCurrentAfterLast = IsCurrentAfterLast; bool oldIsCurrentBeforeFirst = IsCurrentBeforeFirst; SetCurrent(proposedCurrentItem, proposed); OnCurrentChanged(); // notify that the properties have changed. if (IsCurrentAfterLast != oldIsCurrentAfterLast) OnPropertyChanged(IsCurrentAfterLastPropertyName); if (IsCurrentBeforeFirst != oldIsCurrentBeforeFirst) OnPropertyChanged(IsCurrentBeforeFirstPropertyName); OnPropertyChanged(CurrentPositionPropertyName); OnPropertyChanged(CurrentItemPropertyName); } } // subscribe to change notifications private void SubscribeToChanges () { if (InternalList.SupportsChangeNotification) { InternalList.ListChanged += new ListChangedEventHandler(OnListChanged); } } // IBindingList has changed // At this point we may not have entered the UIContext, but // the call to base.OnCollectionChanged will marshall the change over private void OnListChanged(object sender, ListChangedEventArgs args) { if (_ignoreInnerRefresh && (args.ListChangedType == ListChangedType.Reset)) return; NotifyCollectionChangedEventArgs forwardedArgs = null; object item = null; int delta = _isGrouping ? 0 : (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) ? 1 : 0; int index = args.NewIndex; switch (args.ListChangedType) { case ListChangedType.ItemAdded: // Some implementations of IBindingList raise an extra ItemAdded event // when the new item (from a previous call to AddNew) is "committed". // [The IBindingList documentation suggests that all implementations // should do this, but only DataView seems to obey this rather // bizarre requirement.] We will ignore these extra events, unless // they arise from a commit that we initiated. There's // no way to detect them from the event args; we do it the same // way WinForms.DataGridView does - by comparing counts. if (InternalList.Count == _cachedList.Count) { if (IsAddingNew && index == _newItemIndex) { Debug.Assert(_newItem == InternalList[index], "unexpected item while committing AddNew"); forwardedArgs = ProcessCommitNew(index + delta, index + delta); } } else { // normal ItemAdded event item = InternalList[index]; forwardedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index + delta); _cachedList.Insert(index, item); Invariant.Assert(InternalList.Count == _cachedList.Count, "ItemAdded: Expect underlying IBL and cached list to be of same length"); if (index <= _newItemIndex) { ++ _newItemIndex; } } break; case ListChangedType.ItemDeleted: item = _cachedList[index]; _cachedList.RemoveAt(index); Invariant.Assert(InternalList.Count == _cachedList.Count, "ItemDeleted: Expect underlying IBL and cached list to be of same length"); if (index < _newItemIndex) { -- _newItemIndex; } // implicitly cancel AddNew and/or EditItem transactions if the relevant item is removed if (item == CurrentEditItem) { ImplicitlyCancelEdit(); } if (item == CurrentAddItem) { EndAddNew(true); switch (NewItemPlaceholderPosition) { case NewItemPlaceholderPosition.AtBeginning: index = 0; break; case NewItemPlaceholderPosition.AtEnd: index = InternalCount - 1; break; } } forwardedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index + delta); break; case ListChangedType.ItemMoved: if (IsAddingNew && args.OldIndex == _newItemIndex) { // ItemMoved applied to the new item. We assume this is the result // of committing a new item when a sort is in effect - the item // moves to its sorted position. There's no way to verify this assumption. item = _newItem; Debug.Assert(item == InternalList[index], "unexpected item while committing AddNew"); forwardedArgs = ProcessCommitNew(args.OldIndex, index + delta); } else { // normal ItemMoved event item = InternalList[index]; forwardedArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, index+delta, args.OldIndex+delta); if (args.OldIndex < _newItemIndex && _newItemIndex < args.NewIndex) { -- _newItemIndex; } else if (args.NewIndex <= _newItemIndex && _newItemIndex < args.OldIndex) { ++ _newItemIndex; } } _cachedList.RemoveAt(args.OldIndex); _cachedList.Insert(args.NewIndex, item); Invariant.Assert(InternalList.Count == _cachedList.Count, "ItemMoved: Expect underlying IBL and cached list to be of same length"); break; case ListChangedType.ItemChanged: if (!_itemsRaisePropertyChanged.HasValue) { // check whether individual items raise PropertyChanged events // (DataRowView does) item = InternalList[args.NewIndex]; _itemsRaisePropertyChanged = (item is INotifyPropertyChanged); } // if items raise PropertyChanged, we can ignore ItemChanged; // otherwise, treat it like a Reset if (!_itemsRaisePropertyChanged.Value) { goto case ListChangedType.Reset; } break; case ListChangedType.Reset: // treat all other changes like Reset case ListChangedType.PropertyDescriptorAdded: case ListChangedType.PropertyDescriptorChanged: case ListChangedType.PropertyDescriptorDeleted: // implicitly cancel EditItem transactions if (IsEditingItem) { ImplicitlyCancelEdit(); } // adjust AddNew transactions, depending on whether the new item // survived the Reset if (IsAddingNew) { _newItemIndex = InternalList.IndexOf(_newItem); if (_newItemIndex < 0) { EndAddNew(true); } } RefreshOrDefer(); break; } if (forwardedArgs != null) { base.OnCollectionChanged(sender, forwardedArgs); } } // fix up CurrentPosition and CurrentItem after a collection change private void AdjustCurrencyForAdd(int index) { if (InternalCount == 1) { // added first item; set current at BeforeFirst SetCurrent(null, -1); } else if (index <= CurrentPosition) // adjust current index if insertion is earlier { int newPosition = CurrentPosition + 1; if (newPosition < InternalCount) { // CurrentItem might be out of [....] if underlying list is not INCC // or if this Add is the result of a Replace (Rem + Add) SetCurrent(GetItemAt(newPosition), newPosition); } else { SetCurrent(null, InternalCount); } } } // fix up CurrentPosition and CurrentItem after a collection change // return true if the current item was removed private bool AdjustCurrencyForRemove(int index) { bool result = (index == CurrentPosition); // adjust current index if deletion is earlier if (index < CurrentPosition) { SetCurrent(CurrentItem, CurrentPosition - 1); } return result; } // fix up CurrentPosition and CurrentItem after a collection change private void AdjustCurrencyForMove(int oldIndex, int newIndex) { if (oldIndex == CurrentPosition) { // moving the current item - currency moves with the item (bug 1942184) SetCurrent(GetItemAt(newIndex), newIndex); } else if (oldIndex < CurrentPosition && CurrentPosition <= newIndex) { // moving an item from before current position to after - // current item shifts back one position SetCurrent(CurrentItem, CurrentPosition - 1); } else if (newIndex <= CurrentPosition && CurrentPosition < oldIndex) { // moving an item from after current position to before - // current item shifts ahead one position SetCurrent(CurrentItem, CurrentPosition + 1); } // else no change necessary } // fix up CurrentPosition and CurrentItem after a collection change // return true if the current item was replaced private bool AdjustCurrencyForReplace(int index) { bool result = (index == CurrentPosition); if (result) { SetCurrent(GetItemAt(index), index); } return result; } private void MoveCurrencyOffDeletedElement(int oldCurrentPosition) { int lastPosition = InternalCount - 1; // OK if last is -1 // if position falls beyond last position, move back to last position int newPosition = (oldCurrentPosition < lastPosition) ? oldCurrentPosition : lastPosition; OnCurrentChanging(); if (newPosition < 0) SetCurrent(null, newPosition); else SetCurrent(InternalItemAt(newPosition), newPosition); OnCurrentChanged(); } private IList CollectionProxy { get { if (UpdatedOutsideDispatcher) return _shadowList; else return InternalList; } } /// /// Accessor to private _internalList field. /// private IBindingList InternalList { get { return _internalList; } set { _internalList = value; } } private bool IsCustomFilterSet { get { return ((_blv != null) && !String.IsNullOrEmpty(_customFilter)); } } // can the group name(s) for an item change after we've grouped the item? private bool CanGroupNamesChange { // There's no way we can deduce this - the app has to tell us. // If this is true, removing a grouped item is quite difficult. // We cannot rely on its group names to tell us which group we inserted // it into (they may have been different at insertion time), so we // have to do a linear search. get { return true; } } // SortDescription was added/removed, refresh CollView private void SortDescriptionsChanged(object sender, NotifyCollectionChangedEventArgs e) { if (IsAddingNew || IsEditingItem) throw new InvalidOperationException(SR.Get(SRID.MemberNotAllowedDuringAddOrEdit, "Sorting")); RefreshOrDefer(); } // convert from Avalon SortDescriptions to the corresponding .NET collection private ListSortDescriptionCollection ConvertSortDescriptionCollection(SortDescriptionCollection sorts) { ListSortDescriptionCollection convertedSorts = null; // ITypedList itl = InternalList as ITypedList; if (itl != null) { PropertyDescriptorCollection pdc = itl.GetItemProperties(null); if ((pdc == null) || (pdc.Count == 0)) throw new ArgumentException(SR.Get(SRID.CannotDetermineSortByPropertiesForCollection)); ListSortDescription[] sortDescriptions = new ListSortDescription[sorts.Count]; for (int i = 0; i < sorts.Count; i++) { PropertyDescriptor dd = pdc.Find(sorts[i].PropertyName, true); if (dd == null) { string typeName = itl.GetListName(null); throw new ArgumentException(SR.Get(SRID.PropertyToSortByNotFoundOnType, typeName, sorts[i].PropertyName)); } ListSortDescription sd = new ListSortDescription(dd, sorts[i].Direction); sortDescriptions[i] = sd; } convertedSorts = new ListSortDescriptionCollection(sortDescriptions); } else { // cannot resolve property names to PropertyDescriptors -> return emtpy collection convertedSorts = new ListSortDescriptionCollection(new ListSortDescription[] {}); } return convertedSorts; } #region Grouping // divide the data items into groups void PrepareGroups() { // discard old groups _group.Clear(); // initialize the synthetic top level group _group.Initialize(); // if there's no grouping, there's nothing to do _isGrouping = (_group.GroupBy != null); if (!_isGrouping) return; // loop through the sorted/filtered list of items, dividing them // into groups (with special cases for placeholder and new item) if (NewItemPlaceholderPosition == NewItemPlaceholderPosition.AtBeginning) { _group.InsertSpecialItem(0, NewItemPlaceholder, true /*loading*/); if (IsAddingNew) { _group.InsertSpecialItem(1, _newItem, true /*loading*/); } } for (int k=0, n=InternalList.Count; k /// Helper to raise a PropertyChanged event />). /// private void OnPropertyChanged(string propertyName) { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } #endregion Private Methods private class BindingListSortDescriptionCollection : SortDescriptionCollection { internal BindingListSortDescriptionCollection(bool allowMultipleDescriptions) { _allowMultipleDescriptions = allowMultipleDescriptions; } /// /// called by base class ObservableCollection<T> when an item is added to list; /// protected override void InsertItem(int index, SortDescription item) { if (!_allowMultipleDescriptions && (this.Count > 0)) { throw new InvalidOperationException(SR.Get(SRID.BindingListCanOnlySortByOneProperty)); } base.InsertItem(index, item); } private bool _allowMultipleDescriptions; } //----------------------------------------------------- // // Private Fields // //------------------------------------------------------ #region Private Fields private IBindingList _internalList; private CollectionViewGroupRoot _group; private bool _isGrouping; private IBindingListView _blv; private BindingListSortDescriptionCollection _sort; private ArrayList _shadowList; private bool _applyChangeToShadow = false; private bool _isSorted; private IComparer _comparer; private string _customFilter; private bool _isFiltered; private bool _ignoreInnerRefresh; private bool? _itemsRaisePropertyChanged; private bool _isDataView; private object _newItem; private object _editItem; private int _newItemIndex; // position of _newItem in the source collection private NewItemPlaceholderPosition _newItemPlaceholderPosition; // to handle ItemRemoved directly, we need to remember the items - // IBL's event args tell us the index, not the item itself private ArrayList _cachedList; #endregion Private Fields } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.

Link Menu

Network programming in C#, Network Programming in VB.NET, Network Programming in .NET
This book is available now!
Buy at Amazon US or
Buy at Amazon UK