diff --git a/BindingListView/AggregateBindingListView.cs b/BindingListView/AggregateBindingListView.cs new file mode 100644 index 00000000..7c348db7 --- /dev/null +++ b/BindingListView/AggregateBindingListView.cs @@ -0,0 +1,2009 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Collections; +using System.Reflection; +using System.Diagnostics; + +namespace Equin.ApplicationFramework +{ + public class AggregateBindingListView : Component, IBindingListView, IList, IRaiseItemChangedEvents, ICancelAddNew, ITypedList, IEnumerable + { + #region Constructors + + public AggregateBindingListView() + { + _sourceLists = new BindingList(); + (_sourceLists as IBindingList).ListChanged += new ListChangedEventHandler(SourceListsChanged); + _savedSourceLists = new List(); + _sourceIndices = new MultiSourceIndexList(); + // Start with a filter that includes all items. + _filter = IncludeAllItemFilter.Instance; + // Start with no sorts applied. + _sorts = new ListSortDescriptionCollection(); + _objectViewCache = new Dictionary>(); + } + + public AggregateBindingListView(IContainer container) + : this() + { + container.Add(this); + + if (Site is ISynchronizeInvoke) + { + SynchronizingObject = Site as ISynchronizeInvoke; + } + } + + #endregion + + #region Private Member Fields + + /// + /// The list of underlying list of items on which this view is based. + /// + private IList _sourceLists; + /// + /// The sorted, filtered list of item indices in _sourceList. + /// + private MultiSourceIndexList _sourceIndices; + /// + /// The current filter applied to the view. + /// + private IItemFilter _filter; + /// + /// The current sorts applied to the view. + /// + private ListSortDescriptionCollection _sorts; + /// + /// The IComparer used to compare items when sorting. + /// + private IComparer, int>> _comparer; + /// + /// The item in the process of being added to the view. + /// + private ObjectView _newItem; + /// + /// The IList we will add new items to. + /// + private IList _newItemsList; + /// + /// The object used to marshal event-handler calls that are invoked on a non-UI thread. + /// + private ISynchronizeInvoke _synchronizingObject; + /// + /// A copy of the source lists so when a list is removed from SourceLists + /// we still have a reference to use for unhooking events, etc. + /// + private List _savedSourceLists; + /// + /// The property on a source list item that contains the actual list to view. + /// If null or empty then the source list item is used instead. + /// + private string _dataMember; + /// + /// ObjectView cache used to prevent re-creation of existing object wrappers when + /// in FilterAndSort(). + /// + private Dictionary> _objectViewCache; + /// + /// Controls whether or not the view is automatically re-filtered and re-sorted when + /// source lists change. + /// + private bool _autoFilterAndSortSuspended; + + #endregion + + /// + /// Gets or sets the list of source lists used by this view. + /// + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IList SourceLists + { + get + { + return _sourceLists; + } + set + { + if (value == null) + { + throw new ArgumentNullException("SourceLists", Properties.Resources.SourceListsNull); + } + + // Check that every item in each list is of type T. + foreach (object obj in value) + { + if (obj == null) + { + throw new InvalidSourceListException(); + } + + IList list = null; + if (!string.IsNullOrEmpty(DataMember)) + { + foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(obj)) + { + if (pd.Name == DataMember) + { + list = pd.GetValue(obj) as IList; + break; + } + } + } + else if (obj is IListSource) + { + IListSource src = obj as IListSource; + if (src.ContainsListCollection) + { + list = src.GetList()[0] as IList; + } + else + { + list = (obj as IListSource).GetList(); + } + } + else if (!(obj is ICollection)) + { + list = obj as IList; + } + else + { + // We have a typed collection, so can skip the item-by-item check. + continue; + } + + if (list == null) + { + throw new InvalidSourceListException(); + } + + foreach (object item in list) + { + if (!(item is T)) + { + throw new InvalidSourceListException(string.Format(Properties.Resources.InvalidListItemType, typeof(T).FullName)); + } + } + } + + IBindingList bindingList = _sourceLists as IBindingList; + + // Un-hook old list changed event. + if (bindingList != null && bindingList.SupportsChangeNotification) + { + bindingList.ListChanged -= new ListChangedEventHandler(SourceListsChanged); + } + + foreach (object list in _sourceLists) + { + IBindingList bl = list as IBindingList; + if (bl != null && bl.SupportsChangeNotification) + { + bl.ListChanged -= new ListChangedEventHandler(SourceListChanged); + } + } + + _sourceLists = value; + + bindingList = _sourceLists as IBindingList; + // Hook new list changed event + if (bindingList != null && bindingList.SupportsChangeNotification) + { + bindingList.ListChanged += new ListChangedEventHandler(SourceListsChanged); + } + foreach (object list in _sourceLists) + { + IBindingList bl = list as IBindingList; + if (bl != null && bl.SupportsChangeNotification) + { + bl.ListChanged += new ListChangedEventHandler(SourceListChanged); + } + } + + // save new lists + BuildSavedList(); + + FilterAndSort(); + OnListChanged(ListChangedType.Reset, -1); + } + } + + /// + /// Gets the ObjectView<T> of the item at the given index in the view. + /// + /// The item index. + /// The ObjectView<T> of the item. + public ObjectView this[int index] + { + get + { + return _sourceIndices[index].Key.Item; + } + } + + [Browsable(false)] + public string DataMember + { + get + { + return _dataMember; + } + set + { + _dataMember = value; + FilterAndSort(); + OnListChanged(ListChangedType.Reset, -1); + } + } + + private bool ShouldSerializeListMember() + { + return !string.IsNullOrEmpty(DataMember); + } + + #region Adding New Items + + /// + /// Occurs before an item is added to the list. + /// Assign the event argument's NewObject property to provide the object to add. + /// + public event AddingNewEventHandler AddingNew; + + /// + /// Attempts to get a new object to add to the list, first by raising the + /// AddingNew event and then (if no new object was assigned) by using the + /// default public constructor. + /// + /// The new object to add to the list. + /// No new object provided by the AddingNew event handler and has no default public constructor. + protected virtual T OnAddingNew() + { + // We allow users of this class to provide the object to add + // by raising the AddingNew event. + if (AddingNew != null) + { + AddingNewEventArgs args = new AddingNewEventArgs(); + AddingNew(this, args); + // Check if we were given an object (and it's the correct type) + if ((args.NewObject != null) && (args.NewObject is T)) + { + return (T)args.NewObject; + } + } + // Otherwise, try the default public constructor instead. + // Use reflection to find it. Note: We're not using the generic new() constraint since + // we do not want to force the need for a public default constructor when the user + // can simply handle the AddingNew event called above. + System.Reflection.ConstructorInfo ci = typeof(T).GetConstructor(System.Type.EmptyTypes); + if (ci != null) + { + // Invoke the constructor to create the object. + return (T)ci.Invoke(null); + } + else + { + throw new InvalidOperationException(Properties.Resources.CannotAddNewItem); + } + } + + /// + /// Adds a new item to the view. Note that EndNew must be called to commit + /// the item to the to the source list. + /// + /// The new item, wrapped in an ObjectView. + public ObjectView AddNew() + { + // Are we currently adding another item? + if (_newItem != null) + { + // Need to commit previous new item before adding another. + EndNew(_sourceIndices.Count - 1); + } + + // Get the new item to add. + T item = OnAddingNew(); + + // Create the ObjectView wrapper for the item. + ObjectView objectView = new ObjectView(item, this); + + _objectViewCache[item] = objectView; + + HookPropertyChangedEvent(objectView); + + // Set the _newItem reference so we know what to use when ending/cancelling this add operation. + _newItem = objectView; + + // Add to indicies list, but index of -1 means it's not in the source list yet. + _sourceIndices.Add(_newItemsList, objectView, -1); + // Tell any data binders that we've added an item to the view. + // Put it at the end of the list. + OnListChanged(ListChangedType.ItemAdded, _sourceIndices.Count - 1); + + return objectView; + } + + /// + /// Cancels the pending addition of a new item to the source list + /// and remove the item from the view. + /// + /// The index of the new item. + public void CancelNew(int itemIndex) + { + // We must take special care that the item index does refer to the new item. + if (itemIndex > -1 && itemIndex < _sourceIndices.Count && + _newItem != null && _sourceIndices[itemIndex].Key.Item == _newItem) + { + // We no longer need to listen to any events from the object. + UnHookPropertyChangedEvent(_newItem); + // Remove the item from the view. + _sourceIndices.RemoveAt(itemIndex); + // Data binders need to know the item has gone from the view. + OnListChanged(ListChangedType.ItemDeleted, itemIndex); + // Done with this adding operation, so clear the _newItem reference. + _newItem = null; + } + } + + /// + /// Commits the pending addition of a new item to the source list. + /// + /// The index of the new item. + public void EndNew(int itemIndex) + { + // The binding infrastructure tends to call the method + // more times than needed and often with itemIndex not even pointing to the + // new object! So we have to take special care to check. + if (itemIndex > -1 && itemIndex < _sourceIndices.Count && + _newItem != null && _sourceIndices[itemIndex].Key.Item == _newItem) + { + // In order to reuse the SourceListChanged code for adding a new item + // we have to first remove all knowledge of the item, then add it + // to the source list. + + // We no longer need to listen to any events from the object. + UnHookPropertyChangedEvent(_newItem); + // Remove the item from the view. + _sourceIndices.RemoveAt(itemIndex); + + // Add the actual data object to the source list. + // The SourceListChanged event handler will take care of correctly inserting this + // object into the view (if newItemsList is a IBindingList). + _newItemsList.Add(_newItem.Object); + + // If it is not an IBindingList (or not SupportsChangeNotification) + // then we must force the update ourselves. + if (!(_newItemsList is IBindingList) || !(_newItemsList as IBindingList).SupportsChangeNotification) + { + if (!_autoFilterAndSortSuspended) + { + FilterAndSort(); + } + OnListChanged(ListChangedType.Reset, -1); + } + + // Done with this adding operation, so clear the _newItem reference. + _newItem = null; + } + } + + /// + /// Gets or sets the source list to which new items are added. + /// + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IList NewItemsList + { + get + { + return _newItemsList; + } + set + { + if (value != null && !_sourceLists.Contains(value)) + { + throw new ArgumentException(Properties.Resources.SourceListNotFound); + } + _newItemsList = value; + } + } + + #endregion + + /// + /// Re-applies any current filter and sorts to refresh the current view. + /// + public void Refresh() + { + FilterAndSort(); + // Get any bound objects to refresh everything as well. + OnListChanged(ListChangedType.Reset, -1); + } + + /// + /// Gets or sets the object used to marshal event-handler calls that are invoked on a non-UI thread. + /// + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ISynchronizeInvoke SynchronizingObject + { + get + { + return _synchronizingObject; + } + set + { + _synchronizingObject = value; + } + } + + public void SuspendAutoFilterAndSort() + { + _autoFilterAndSortSuspended = true; + } + + public void ResumeAutoFilterAndSort() + { + _autoFilterAndSortSuspended = false; + } + + /// + /// Updates the _sourceIndices list to contain the items that are current viewed + /// according to applied filter and sorts. + /// + protected void FilterAndSort() + { + // The view contains items from the source list + // and possibly a new items that are not yet committed. + // Therefore we can't just clear the list and start over + // as we would lose the new items. So we have to to insert + // filtered source list items into a new list first. + // New items can then be pulled out of the current view + // and appended to the new list. + MultiSourceIndexList newList = new MultiSourceIndexList(); + + // Get items from the source list that are included by the current filter. + foreach (IList sourceList in GetSourceLists()) + { + for (int i = 0; i < sourceList.Count; i++) + { + T item = (T)sourceList[i]; + ObjectView editableObject; + if (_filter.Include(item)) + { + if (_objectViewCache.ContainsKey(item)) + { + editableObject = _objectViewCache[item]; + } + else + { + editableObject = new ObjectView(item, this); + _objectViewCache.Add(item, editableObject); + // Listen to the editing notification and property changed events. + HookEditableObjectEvents(editableObject); + HookPropertyChangedEvent(editableObject); + } + + // Add the editable object along with the index of the item in the source list. + newList.Add(sourceList, editableObject, i); + } + else + { + if (_objectViewCache.ContainsKey(item)) + { + editableObject = _objectViewCache[item]; + UnHookEditableObjectEvents(editableObject); + UnHookPropertyChangedEvent(editableObject); + _objectViewCache.Remove(item); + } + } + } + } + + // If we have sorts to apply, do them now + if (_comparer != null) + { + newList.Sort(_comparer); + } + + // Now we can append any new items to the end of the view. + foreach (KeyValuePair, int> kvp in _sourceIndices) + { + // New items have a source list index of -1 since they are not + // yet in the source list. + if (kvp.Value == -1) + { + newList.Add(kvp); + } + } + + // Set our view now + _sourceIndices = newList; + + // Note: We do not raise the ListChanged event with ListChangeType.Reset + // since the view may not have changed that much. It is better to let + // the calling code decide what has happened and raise events accordingly. + } + + #region Editing Items Event Handlers + + /// + /// Currently unused. Here in case we want to perform actions when + /// an item edit begins. + /// + protected virtual void BegunItemEdit(object sender, EventArgs e) + { + + } + + /// + /// Currently unused. Here in case we want to perform actions when + /// an item edit is cancelled. + /// + protected virtual void CancelledItemEdit(object sender, EventArgs e) + { + + } + + /// + /// Handles the EndedEdit event. + /// + /// The that raised the event. + protected virtual void EndedItemEdit(object sender, EventArgs e) + { + if (_autoFilterAndSortSuspended) + { + return; + } + + ObjectView editableObject = (ObjectView)sender; + + // Check if filtering removed the item from view + // by getting the index before and after + int oldIndex = _sourceIndices.IndexOfItem(editableObject.Object); + FilterAndSort(); + int newIndex = _sourceIndices.IndexOfItem(editableObject.Object); + // if item was filtered out then the newIndex == -1 + if (newIndex > -1) + { + if (oldIndex == newIndex) + { + OnListChanged(ListChangedType.ItemChanged, newIndex); + } + else + { + OnListChanged(ListChangedType.ItemMoved, newIndex, oldIndex); + } + } + else + { + OnListChanged(ListChangedType.ItemDeleted, oldIndex); + } + } + + #endregion + + /// + /// Event handler for when SourceLists is changed. + /// + protected virtual void SourceListsChanged(object sender, ListChangedEventArgs e) + { + if (e.ListChangedType == ListChangedType.ItemAdded) + { + IList list = SourceLists[e.NewIndex] as IList; + if (list == null) + { + SourceLists.RemoveAt(e.NewIndex); + throw new InvalidSourceListException(); + } + + if (list is IBindingList) + { + // We need to know when the source list changes + (list as IBindingList).ListChanged += new ListChangedEventHandler(SourceListChanged); + } + _savedSourceLists.Add(list); + if (!_autoFilterAndSortSuspended) + { + FilterAndSort(); + OnListChanged(ListChangedType.Reset, -1); + } + } + else if (e.ListChangedType == ListChangedType.ItemDeleted) + { + IList list = _savedSourceLists[e.NewIndex] as IList; + if (list != null) + { + if (list is IBindingList) + { + (list as IBindingList).ListChanged -= new ListChangedEventHandler(SourceListChanged); + } + _savedSourceLists.RemoveAt(e.NewIndex); + if (!_autoFilterAndSortSuspended) + { + FilterAndSort(); + OnListChanged(ListChangedType.Reset, -1); + } + } + } + else if (e.ListChangedType == ListChangedType.Reset) + { + BuildSavedList(); + if (!_autoFilterAndSortSuspended) + { + FilterAndSort(); + OnListChanged(ListChangedType.Reset, -1); + } + } + } + + /// + /// Event handler for when a source list changes. + /// + private void SourceListChanged(object sender, ListChangedEventArgs e) + { + if (_autoFilterAndSortSuspended) + { + return; + } + + int oldIndex; + int newIndex; + IBindingList sourceList = sender as IBindingList; + switch (e.ListChangedType) + { + case ListChangedType.ItemAdded: + FilterAndSort(); + // Get the index of the newly sorted item + newIndex = _sourceIndices.IndexOfSourceIndex(sourceList, e.NewIndex); + if (newIndex > -1) + { + OnListChanged(ListChangedType.ItemAdded, newIndex); + // Other items have moved down the list + for (int i = newIndex + 1; i < Count; i++) + { + OnListChanged(ListChangedType.ItemMoved, i - 1, i); + } + } + else + { + // The item was excluded by the filter, + // so to the viewer the item has been "deleted". + // The new item will have been added at the end of the view + OnListChanged(ListChangedType.ItemDeleted, Math.Max(Count - 1, 0)); + } + break; + + case ListChangedType.ItemChanged: + // Check if filtering will remove the item from view + // by getting the index before and after + oldIndex = _sourceIndices.IndexOfSourceIndex(sourceList, e.NewIndex); + + // Is the object in our view? + if (oldIndex < 0) + { + return; + } + + FilterAndSort(); + newIndex = _sourceIndices.IndexOfSourceIndex(sourceList, e.NewIndex); + // if item was filtered out then the newIndex == -1 + // otherwise we can say that the item was changed. + if (newIndex > -1) + { + if (newIndex == oldIndex) + { + OnListChanged(ListChangedType.ItemChanged, newIndex); + } + else + { + // Two items will have changed places + OnListChanged(ListChangedType.ItemMoved, newIndex, oldIndex); + } + } + else + { + OnListChanged(ListChangedType.ItemDeleted, oldIndex); + } + break; + + case ListChangedType.ItemDeleted: + // Find the deleted index + newIndex = _sourceIndices.IndexOfSourceIndex(sourceList, e.NewIndex); + + // Did we have the object in our view? + if (newIndex < 0) + { + return; + } + + // Stop listening to it's events + UnHookEditableObjectEvents(_sourceIndices[newIndex].Key.Item); + UnHookPropertyChangedEvent(_sourceIndices[newIndex].Key.Item); + // Remove its index + _sourceIndices.RemoveAt(newIndex); + // Move up indices after removed item + for (int i = 0; i < _sourceIndices.Count; i++) + { + if (_sourceIndices[i].Value > e.NewIndex) + { + _sourceIndices[i] = new KeyValuePair, int>(_sourceIndices[i].Key, _sourceIndices[i].Value - 1); + } + } + // Inform listeners that an item has been deleted from this view + OnListChanged(ListChangedType.ItemDeleted, newIndex); + break; + + case ListChangedType.ItemMoved: + if (!IsSorted && (Filter is IncludeAllItemFilter)) + { + // We can move the item in the view + // note indicies match those in _sourceList + OnListChanged(ListChangedType.ItemMoved, e.NewIndex, e.OldIndex); + } + // Otherwise it makes no sense to move due to sort and/or filter + break; + + case ListChangedType.Reset: + // Most of the source list has changed + // so re-sort and filter + FilterAndSort(); + // The view is most likely to have changed lots as well + OnListChanged(ListChangedType.Reset, -1); + break; + } + } + + /// + /// Event handler for when an item in the view changes. + /// + /// The item that changed. + private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e) + { + // The changed item may not actually be present in the view + int index = _sourceIndices.IndexOfItem((T)sender); + // Test the returned index, -1 => not in the view. + if (index > -1) + { + // Tell listeners that an item has changed. + // This is inline with the IRaiseItemChangedEvents implementation. + OnListChanged(ListChangedType.ItemChanged, index); + } + } + + #region ListChanged Event + + /// + /// Occurs when the list changes or an item in the list changes. + /// + public event ListChangedEventHandler ListChanged; + + /// + /// Raises the ListChanged event with the given event arguments. + /// + /// The ListChangedEventArgs to raise the event with. + protected virtual void OnListChanged(ListChangedEventArgs e) + { + if (ListChanged != null) + { + // Check if we need to invoke on the UI thread or not + if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) + { + SynchronizingObject.Invoke(ListChanged, new object[] { this, e }); + } + else + { + ListChanged(this, e); + } + } + } + + /// + /// Helper method to build the ListChangedEventArgs needed for the ListChanged event. + /// + /// The type of change that occured. + /// The index of the changed item. + private void OnListChanged(ListChangedType listChangedType, int newIndex) + { + OnListChanged(new ListChangedEventArgs(listChangedType, newIndex)); + } + + /// + /// Helper method to build the ListChangedEventArgs needed for the ListChanged event. + /// + /// The type of change that occured. + /// The index of the item after the change. + /// The index of the iem before the change. + private void OnListChanged(ListChangedType listChangedType, int newIndex, int oldIndex) + { + OnListChanged(new ListChangedEventArgs(listChangedType, newIndex, oldIndex)); + } + + #endregion + + #region Filtering + + public void ApplyFilter(IItemFilter filter) + { + Filter = filter; + } + + public void ApplyFilter(Predicate includeItem) + { + if (includeItem == null) + { + throw new ArgumentNullException("includeItem", Properties.Resources.IncludeDelegateCannotBeNull); + } + + Filter = AggregateBindingListView.CreateItemFilter(includeItem); + } + + /// + /// Gets if this view supports filtering of items. Always returns true. + /// + bool IBindingListView.SupportsFiltering + { + get { return true; } + } + + /// Explicitly implemented to expose the stronger Filter property instead. + string IBindingListView.Filter + { + get + { + return Filter.ToString(); + } + set + { + throw new NotSupportedException("Cannot set filter from string expression."); + //TODO: Re-instate this line once we have an expression filter + //Filter = new ExpressionItemFilter(value); + } + } + + /// + /// Gets or sets the filter currently applied to the view. + /// + public IItemFilter Filter + { + get + { + return _filter; + } + set + { + // Do not allow a null filter. Instead, use the "include all items" filter. + if (value == null) value = IncludeAllItemFilter.Instance; + if (_filter != value) + { + _filter = value; + FilterAndSort(); + // The list has probably changed a lot, so get bound controls to reset. + OnListChanged(ListChangedType.Reset, -1); + } + } + } + + private bool ShouldSerializeFilter() + { + return (Filter != IncludeAllItemFilter.Instance); + } + + public static IItemFilter CreateItemFilter(Predicate predicate) + { + if (predicate == null) + { + throw new ArgumentNullException("predicate"); + } + return new PredicateItemFilter(predicate); + } + + // Function for LINQ style filtering + // e.g. SetFilter(i => i.Items.Count < 42) + /* + public static void ApplyFilter(Func predicate) + { + if (predicate == null) + { + throw new ArgumentNullException("predicate"); + } + return new FuncItemFilter(predicate); + } + + // Class to wrap a LINQ Func delegate and expose + // it as an IItemFilter. + private class FuncItemFilter : IItemFilter + { + private Func _func; + + public FuncItemFilter(Func func) + { + _func = func; + } + + public bool Include(T item) + { + return _func(item); + } + } + + */ + + /// + /// Removes any currently applied filter so that all items are displayed by the view. + /// + public void RemoveFilter() + { + // Set filter back to including all items. + Filter = IncludeAllItemFilter.Instance; + } + + #endregion + + #region Sorting + + /// + /// Used to signal that a sort on a property is to be descending, not ascending. + /// + public readonly string SortDescendingModifier = "DESC"; + /// + /// The character used to seperate sorts by multiple properties. + /// + public readonly char SortDelimiter = ','; + + /// + /// Gets if this view supports sorting. Always returns true. + /// + bool IBindingList.SupportsSorting + { + get { return true; } + } + + /// + /// Gets if this view supports advanced sorting. Always returns true. + /// + bool IBindingListView.SupportsAdvancedSorting + { + get { return true; } + } + + /// + /// Sorts the view by a single property in a given direction. + /// This will remove any existing sort. + /// + /// A property of to sort by. + /// The direction to sort in. + public void ApplySort(PropertyDescriptor property, ListSortDirection direction) + { + // Apply sort by setting the current sort descriptions + // to be a collection containing just one SortDescription. + SortDescriptions = new ListSortDescriptionCollection( + new ListSortDescription[] { + new ListSortDescription(property, direction)}); + } + + /// + /// Sorts the view by the given collection of sort descriptions. + /// + /// The sorts to apply. + public void ApplySort(ListSortDescriptionCollection sorts) + { + SortDescriptions = sorts; + } + + /// + /// Sorts the view according to the properties and directions given in the + /// SQL style sort parameter. + /// + /// + /// The SQL ORDER BY clause style sort. + /// A comma separated list of properties to sort by. + /// Use "DESC" after a property name to sort descending. + /// The default direction is ascending. + /// + /// view.ApplySort("Surname, FirstName, Age DESC"); + public void ApplySort(string sort) + { + if (string.IsNullOrEmpty(sort)) + { + RemoveSort(); + return; + } + + // Parse string for sort descriptions + string[] sorts = sort.Split(SortDelimiter); + ListSortDescription[] col = new ListSortDescription[sorts.Length]; + for (int i = 0; i < sorts.Length; i++) + { + // Get the sort description. + // This will be a name optionally followed by a direction. + sort = sorts[i].Trim(); + // A space will separate name from direction. + int pos = sort.IndexOf(' '); + string name; + ListSortDirection direction; + if (pos == -1) + { + // No direction specified, default to ascending. + name = sort; + direction = ListSortDirection.Ascending; + } + else + { + // Name is everything before the space. + name = sort.Substring(0, pos); + // direction is everything after the space. + string dir = sort.Substring(pos + 1).Trim(); + // Check what kind of direction is specified. + // (Ignoring case and culture.) + if (string.Compare(dir, SortDescendingModifier, true, System.Globalization.CultureInfo.InvariantCulture) == 0) + { + direction = ListSortDirection.Descending; + } + else + { + // Default to ascending. + direction = ListSortDirection.Ascending; + } + } + + // Put the sort description into the collection. + col[i] = CreateListSortDescription(name, direction); + } + + ApplySort(new ListSortDescriptionCollection(col)); + } + + public void ApplySort(IComparer comparer) + { + if (comparer == null) + { + throw new ArgumentNullException("comparer"); + } + + // Clear any current sorts + _sorts = new ListSortDescriptionCollection(); + // Sort with this new comparer + _comparer = new ExternalSortComparer(comparer); + FilterAndSort(); + OnListChanged(ListChangedType.Reset, -1); + } + + public void ApplySort(Comparison comparison) + { + if (comparison == null) + { + throw new ArgumentNullException("comparison"); + } + + // Clear any current sorts + _sorts = new ListSortDescriptionCollection(); + // Sort with this new comparer + _comparer = new ExternalSortComparison(comparison); + FilterAndSort(); + OnListChanged(ListChangedType.Reset, -1); + } + + /// + /// Removes any sort currently applied to the view, restoring it to the order of the source list. + /// + public void RemoveSort() + { + // An empty collection of sorts will achieve what we need. + SortDescriptions = new ListSortDescriptionCollection(); + } + + /// + /// Gets if the view is currently sorted. + /// + [Browsable(false)] + public bool IsSorted + { + get + { + // To be sorted there must be some sorts applied. + return (SortDescriptions.Count > 0); + } + } + + /// + /// Gets or sets the string representation of the sort currently applied to the view. + /// + public string Sort + { + get + { + if (IsSorted) + { + // Build a string of the properties being sorted by + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + foreach (ListSortDescription sort in SortDescriptions) + { + sb.Append(sort.PropertyDescriptor.Name); + // Need to signal descending sorts + if (sort.SortDirection == ListSortDirection.Descending) + { + sb.Append(' ').Append(SortDescendingModifier); + } + // Separate by SortDelimiter + sb.Append(SortDelimiter); + } + // Remove trailing SortDelimiter + sb.Remove(sb.Length - 1, 1); + // Return the string + return sb.ToString(); + } + return string.Empty; + } + set + { + ApplySort(value); + } + } + + private bool ShouldSerializeSort() + { + return !String.IsNullOrEmpty(Sort); + } + + /// + /// Gets the direction in which the view is sorted. + /// If more than one sort is applied, the direction of the first is returned. + /// + [Browsable(false)] + public ListSortDirection SortDirection + { + get + { + if (IsSorted) + { + return SortDescriptions[0].SortDirection; + } + else + { + // We don't really want to throw exceptions. + // Calling code should have checked IsSorted to know the true situation. + return ListSortDirection.Ascending; + } + } + } + + /// + /// Gets the property the view is currently sorted by. + /// If more than one sort is applied, the property of the first is returned. + /// + [Browsable(false)] + public PropertyDescriptor SortProperty + { + get + { + if (IsSorted) + { + return SortDescriptions[0].PropertyDescriptor; + } + else + { + // We don't really want to throw exceptions. + // Calling code should have checked IsSorted to know the true situation. + return null; + } + } + } + + /// + /// Gets the sorts currently applied to the view. + /// + [Browsable(false)] + public ListSortDescriptionCollection SortDescriptions + { + get + { + return _sorts; + } + private set + { + _sorts = value; + _comparer = new SortComparer(value); + FilterAndSort(); + // Most of the list will have probably changed, so get bound objects to reset. + OnListChanged(ListChangedType.Reset, -1); + } + } + + /// + /// Used to compare items in the view when sorting the _sourceIndices list. + /// It supports mutliple sorts by different properties and directions. + /// + private class SortComparer : IComparer, int>> + { + private Dictionary> _comparisons; + + /// + /// Creates a new SortComparer that will use the given sorts. + /// + /// The sorts to apply to the view. + public SortComparer(ListSortDescriptionCollection sorts) + { + _sorts = sorts; + + // Build the delegates used to compare properties of objects + _comparisons = new Dictionary>(); + foreach (ListSortDescription sort in sorts) + { + _comparisons[sort] = BuildComparison(sort.PropertyDescriptor.Name, sort.SortDirection); + } + } + + private ListSortDescriptionCollection _sorts; + + /// + /// Compares two items according to the defined sorts. + /// + /// + /// Use of light-weight code generation comparison delegates gives ~10x speed up + /// compared to the pure reflection based implementation. + /// + /// The first item to compare. + /// The second item to compare. + /// -1 if x < y, 0 if x = y and 1 if x > y. + public int Compare(KeyValuePair, int> x, KeyValuePair, int> y) + { + foreach (ListSortDescription sort in _sorts) + { + int result = _comparisons[sort](x.Key.Item.Object, y.Key.Item.Object); + if (result != 0) + { + return result; + } + } + return 0; + } + + private static Comparison BuildComparison(string propertyName, ListSortDirection direction) + { + PropertyInfo pi = typeof(T).GetProperty(propertyName); + Debug.Assert(pi != null, string.Format("Property '{0}' is not a member of type '{1}'", propertyName, typeof(T).FullName)); + + Type pType = pi.PropertyType; + bool isNullable = pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>); + + if (isNullable) + pType = pi.PropertyType.GetGenericArguments()[0]; + + if (typeof(IComparable).IsAssignableFrom(pType)) + { + if (pType.IsValueType && !isNullable) + { + return delegate (T x, T y) + { + return (pi.GetValue(x, null) as IComparable).CompareTo(pi.GetValue(y, null)); + }; + } + else + { + return delegate (T x, T y) + { + int result; + object value1 = pi.GetValue(x, null); + object value2 = pi.GetValue(y, null); + if (value1 != null && value2 != null) + result = (value1 as IComparable).CompareTo(value2); + else if (value1 == null && value2 != null) + result = -1; + else if (value1 != null && value2 == null) + result = 1; + else + result = 0; + + if (direction == ListSortDirection.Descending) + result *= -1; + return result; + }; + } + } + else + { + return delegate (T o1, T o2) + { + if (o1.Equals(o2)) + { + return 0; + } + else + { + return o1.ToString().CompareTo(o2.ToString()); + } + }; + } + } + } + + private class ExternalSortComparer : IComparer, int>> + { + public ExternalSortComparer(IComparer comparer) + { + _comparer = comparer; + } + + private IComparer _comparer; + + public int Compare(KeyValuePair, int> x, KeyValuePair, int> y) + { + return _comparer.Compare(x.Key.Item.Object, y.Key.Item.Object); + } + } + + private class ExternalSortComparison : IComparer, int>> + { + public ExternalSortComparison(Comparison comparison) + { + _comparison = comparison; + } + + private Comparison _comparison; + + public int Compare(KeyValuePair, int> x, KeyValuePair, int> y) + { + return _comparison(x.Key.Item.Object, y.Key.Item.Object); + } + } + + #endregion + + #region Searching + + /// + /// Gets if this view supports searching using the Find method. Always returns true. + /// + bool IBindingList.SupportsSearching + { + get { return true; } + } + + /// + /// Returns the index of the first item in the view who's property equals the given value. + /// -1 is returned if no item is found. + /// + /// The property of each item to check. + /// The value being sought. + /// The index of the item, or -1 if not found. + public int Find(PropertyDescriptor property, object key) + { + for (int i = 0; i < _sourceIndices.Count; i++) + { + if (property.GetValue(_sourceIndices[i].Key.Item.Object).Equals(key)) + { + return i; + } + } + return -1; + } + + /// + /// Returns the index of the first item in the view who's property equals the given value. + /// -1 is returned if no item is found. + /// + /// The property name of each item to check. + /// The value being sought. + /// The index of the item, or -1 if not found. + /// + /// It is easier for users of this class to enter a property name + /// and get the PropertyDescriptor ourselves. + /// + public int Find(string propertyName, object key) + { + PropertyDescriptor pd = GetPropertyDescriptor(propertyName); + if (pd != null) + { + return Find(pd, key); + } + else + { + throw new ArgumentException(string.Format(Properties.Resources.PropertyNotFound, propertyName, typeof(T).FullName), "propertyName"); + } + } + + #endregion + + #region IBindingList Members + + /// + /// Gets if this view raises the ListChanged event. Always returns true. + /// + bool IBindingList.SupportsChangeNotification + { + get { return true; } + } + + /// Explicitly implemented so the type safe AddNew method is exposed instead. + object IBindingList.AddNew() + { + return this.AddNew(); + } + + /// + /// Gets if this view allows items to be edited. + /// + /// Delegates to the source list. + bool IBindingList.AllowEdit + { + get + { + foreach (object list in SourceLists) + { + if (list is IBindingList) + { + if (!(list as IBindingList).AllowEdit) + { + return false; + } + } + } + return true; + } + } + + /// + /// Gets if this view allows new items to be added using AddNew(). + /// + /// Delegates to the source list. + bool IBindingList.AllowNew + { + get + { + if (_newItemsList != null) + { + if (_newItemsList is IBindingList) + { + // Respect what the binding list says. + return (_newItemsList as IBindingList).AllowNew; + } + // _newItemsList is a IList, so we can call Add() + // it may fail at runtime - but that is the callee's problem + return true; + } + return false; + } + } + + /// + /// Gets if this view allows items to be removed. + /// + /// Delegates to the source list. + bool IBindingList.AllowRemove + { + get + { + foreach (object list in SourceLists) + { + if (list is IBindingList) + { + if (!(list as IBindingList).AllowRemove) + { + return false; + } + } + } + return true; + } + } + + /// + /// Not implemented. + /// + /// Method not implemented. + void IBindingList.AddIndex(PropertyDescriptor property) + { + throw new NotImplementedException(); + } + + /// + /// Not implemented. + /// + /// Method not implemented. + void IBindingList.RemoveIndex(PropertyDescriptor property) + { + throw new NotImplementedException(); + } + + #endregion + + #region IRaiseItemChangedEvents Members + + /// + /// Gets if this view raises the ListChanged event when an item changes. Always returns true. + /// + [Browsable(false)] + public bool RaisesItemChangedEvents + { + get { return true; } + } + + #endregion + + #region IList Members + + /// + /// value is of the wrong type. + /// + /// + /// is null, so an item cannot be added. + /// + int IList.Add(object value) + { + if (value == null) + { + AddNew(); + return Count - 1; + } + + throw new NotSupportedException(Properties.Resources.CannotAddItem); + } + + /// + /// Cannot clear this view. + /// + /// + /// Cannot clear this view. + /// + void IList.Clear() + { + throw new NotSupportedException(Properties.Resources.CannotClearView); + } + + /// + /// Checks if this view contains the given item. + /// Note that items excluded by current filter are not searched. + /// + /// The item to search for. + /// True if the item is in the view, else false. + bool IList.Contains(object item) + { + // See if the source indices contain the item + if (item is ObjectView) + { + return _sourceIndices.ContainsKey((ObjectView)item); + } + else if (item is T) + { + return _sourceIndices.ContainsItem((T)item); + } + else + { + return false; + } + } + + /// + /// Gets the index in the view of an item. + /// + /// The item to search for + /// The index of the item, or -1 if not found. + int IList.IndexOf(object item) + { + if (item is ObjectView) + { + return _sourceIndices.IndexOfKey(item as ObjectView); + } + else if (item is T) + { + return _sourceIndices.IndexOfItem((T)item); + } + return -1; + } + + /// + /// Cannot insert an external item into this collection. + /// + /// + /// Cannot insert an external item into this collection. + /// + void IList.Insert(int index, object value) + { + throw new NotSupportedException(Properties.Resources.CannotInsertItem); + } + + /// + /// Gets a value indicating if this view is read-only. + /// + /// Delegates to the source list. + bool IList.IsReadOnly + { + get + { + foreach (object list in SourceLists) + { + if (list is IBindingList) + { + if (!(list as IBindingList).IsReadOnly) + { + return false; + } + } + else + { + return false; + } + } + return true; + } + } + + /// + /// Always returns false because the view can change size when + /// source lists are added. + /// + bool IList.IsFixedSize + { + get + { + return false; + } + } + + /// + /// Removes the given item from the view and underlying source list. + /// + /// Either an ObjectView<T> or T to remove. + void IList.Remove(object value) + { + int index = (this as IList).IndexOf(value); + (this as IList).RemoveAt(index); + } + + /// + /// Removes the item from the view at the given index. + /// + /// The index of the item to remove. + void IList.RemoveAt(int index) + { + // Get the index in the source list. + int sourceIndex = _sourceIndices[index].Value; + IList sourceList = _sourceIndices[index].Key.List; + if (sourceIndex > -1) + { + sourceList.RemoveAt(sourceIndex); + if (!(sourceList is IBindingList) || !(sourceList as IBindingList).SupportsChangeNotification) + { + FilterAndSort(); + OnListChanged(ListChangedType.Reset, -1); + } + } + else + { + // The item is not in the source list yet as it is new + // So cancel the new operation instead. + CancelNew(index); + } + } + + /// + /// Gets the at the given index. + /// + /// The index of the item to retrieve. + /// An object. + /// + /// Cannot set an item in the view. + /// + object IList.this[int index] + { + get + { + return this[index]; + } + set + { + // The interface requires we supply a setter + // But we don't want external code modifying the view + // in this manner. + throw new NotSupportedException(Properties.Resources.CannotSetItem); + } + } + + #endregion + + #region ICollection Members + + /// + /// Copies the objects of the view to an , starting at a particular System.Array index. + /// + /// The one-dimensional that is the destination of the elements copied from view. The System.Array must have zero-based indexing. + /// The zero-based index in array at which copying begins. + void ICollection.CopyTo(Array array, int index) + { + _sourceIndices.Keys.CopyTo(array, index); + } + + /// + /// Gets a value indicating whether access to the is synchronized (thread safe). + /// + bool ICollection.IsSynchronized + { + get { return false; } + } + + /// + /// Not supported. + /// + object ICollection.SyncRoot + { + get { throw new NotSupportedException(Properties.Resources.SyncAccessNotSupported); } + } + + /// + /// Gets the number of items currently in the view. This does not include those items + /// excluded by the current filter. + /// + [Browsable(false)] + public int Count + { + get { return _sourceIndices.Count; } + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + for (int i = 0; i < _sourceIndices.Count; i++) + yield return _sourceIndices[i].Key.Item.Object; + + } + + /// + /// Returns an enumerator that iterates through all the items in the view. + /// This does not include those items excluded by the current filter. + /// + /// An IEnumerator to iterate with. + IEnumerator IEnumerable.GetEnumerator() + { + return _sourceIndices.GetKeyEnumerator(); + } + + #endregion + + #region ITypedList Members + + /// + /// Returns the that represents the properties on each item used to bind data. + /// + /// Array of property descriptors to navigate object hirerachy to actual item object. It can be null. + /// The System.ComponentModel.PropertyDescriptorCollection that represents the properties on each item used to bind data. + PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors) + { + PropertyDescriptorCollection originalProps; + + IEnumerator lists = GetSourceLists().GetEnumerator(); + + if (lists.MoveNext() && lists.Current is ITypedList) + { + // Ask the source list for the properties. + originalProps = (lists.Current as ITypedList).GetItemProperties(listAccessors); + } + else + { + // Get the properties ourself. + originalProps = System.Windows.Forms.ListBindingHelper.GetListItemProperties(typeof(T), listAccessors); + } + + if (listAccessors != null && listAccessors.Length > 0) + { + Type type = originalProps[0].ComponentType; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ObjectView<>)) + { + originalProps = originalProps[0].GetChildProperties(); + } + } + + List newProps = new List(); + foreach (PropertyDescriptor pd in AddProvidedViews(originalProps)) + { + newProps.Add(pd); + } + + return new PropertyDescriptorCollection(newProps.ToArray()); + } + + protected internal bool ShouldProvideView(PropertyDescriptor property) + { + return ProvidedViewPropertyDescriptor.CanProvideViewOf(property); + } + + protected internal string GetProvidedViewName(PropertyDescriptor sourceListProperty) + { + return sourceListProperty.Name + "View"; + } + + protected internal object CreateProvidedView(ObjectView @object, PropertyDescriptor sourceListProperty) + { + object list = sourceListProperty.GetValue(@object); + Type viewType = GetProvidedViewType(sourceListProperty); + return Activator.CreateInstance(viewType, list); + } + + private static Type GetProvidedViewType(PropertyDescriptor sourceListProperty) + { + // The source list property type implements IList. + // We want to get the type of X. + Type typeParam = null; + foreach (Type interfaceType in sourceListProperty.PropertyType.GetInterfaces()) + { + if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition().Equals(typeof(IList<>))) + { + typeParam = interfaceType.GetGenericArguments()[0]; + } + } + Debug.Assert(typeParam != null, "Did not get the generic argument for type " + sourceListProperty.PropertyType.FullName); + + // Now build the BindingListView type. + Type viewTypeDef = typeof(BindingListView<>); + Type viewType = viewTypeDef.MakeGenericType(typeParam); + return viewType; + } + + internal IEnumerable AddProvidedViews(PropertyDescriptorCollection properties) + { + if (properties.Count < 0) + { + yield break; + } + foreach (PropertyDescriptor prop in properties) + { + if (ShouldProvideView(prop)) + { + string name = GetProvidedViewName(prop); + yield return new ProvidedViewPropertyDescriptor(name, GetProvidedViewType(prop)); + } + yield return prop; + } + } + + /// + /// Gets the name of the view. + /// + /// Unused. Can be null. + /// The name of the view. + string ITypedList.GetListName(PropertyDescriptor[] listAccessors) + { + return GetType().Name; + } + + #endregion + + #region Helper Methods + + /// + /// Creates a new for given property name and sort direction. + /// + /// The name of the property to sort by. + /// The direction in which to sort. + /// A ListSortDescription. + /// + /// Used by external code to simplify sorting the view. + /// + public ListSortDescription CreateListSortDescription(string propertyName, ListSortDirection direction) + { + PropertyDescriptor pd = GetPropertyDescriptor(propertyName); + if (pd == null) + { + throw new ArgumentException(string.Format(Properties.Resources.PropertyNotFound, propertyName, typeof(T).FullName), "propertyName"); + } + return new ListSortDescription(pd, direction); + } + + /// + /// Gets the property descriptor for a given property name. + /// + /// The name of a property of . + /// The . + private PropertyDescriptor GetPropertyDescriptor(string propertyName) + { + return TypeDescriptor.GetProperties(typeof(T)).Find(propertyName, false); + } + + /// + /// Attaches event handlers to the given 's + /// edit life cycle notification events. + /// + /// The to listen to. + private void HookEditableObjectEvents(ObjectView editableObject) + { + editableObject.EditBegun += new EventHandler(BegunItemEdit); + editableObject.EditCancelled += new EventHandler(CancelledItemEdit); + editableObject.EditEnded += new EventHandler(EndedItemEdit); + } + + /// + /// Detaches event handlers from the given 's + /// edit life cycle notification events. + /// + /// The to stop listening to. + private void UnHookEditableObjectEvents(ObjectView editableObject) + { + editableObject.EditBegun -= new EventHandler(BegunItemEdit); + editableObject.EditCancelled -= new EventHandler(CancelledItemEdit); + editableObject.EditEnded -= new EventHandler(EndedItemEdit); + } + + /// + /// Attaches an event handler to the 's PropertyChanged event. + /// + /// The to listen to. + private void HookPropertyChangedEvent(ObjectView editableObject) + { + editableObject.PropertyChanged += new PropertyChangedEventHandler(ItemPropertyChanged); + } + + /// + /// Detaches the event handler from the 's PropertyChanged event. + /// + /// The to stop listening to. + private void UnHookPropertyChangedEvent(ObjectView editableObject) + { + editableObject.PropertyChanged -= new PropertyChangedEventHandler(ItemPropertyChanged); + } + + private void BuildSavedList() + { + _savedSourceLists.Clear(); + foreach (object list in GetSourceLists()) + { + _savedSourceLists.Add(list as IList); + } + } + + protected IEnumerable GetSourceLists() + { + foreach (object obj in _sourceLists) + { + if (!string.IsNullOrEmpty(DataMember)) + { + bool found = false; + foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(obj)) + { + if (pd.Name == DataMember) + { + found = true; + yield return pd.GetValue(obj) as IList; + break; + } + } + if (!found) + { + yield return null; + } + } + else if (obj is IListSource) + { + IListSource src = obj as IListSource; + if (src.ContainsListCollection) + { + IList list = src.GetList() as IList; + if (list != null && list.Count > 0) + { + list = list[0] as IList; + yield return list; + } + else + { + yield return null; + } + } + else + { + yield return src.GetList(); + } + } + else + { + yield return obj as IList; + } + } + } + + #endregion + + } +} diff --git a/BindingListView/BindingListView.cs b/BindingListView/BindingListView.cs new file mode 100644 index 00000000..d43c20ae --- /dev/null +++ b/BindingListView/BindingListView.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Equin.ApplicationFramework +{ + /// + /// A searchable, sortable, filterable, data bindable view of a list of objects. + /// + /// The type of object in the list. + public class BindingListView : AggregateBindingListView + { + /// + /// Creates a new of a given IBindingList. + /// All items in the list must be of type . + /// + /// The list of objects to base the view on. + public BindingListView(IList list) + : base() + { + DataSource = list; + } + + public BindingListView(IContainer container) + : base(container) + { + DataSource = null; + } + + [DefaultValue(null)] + [AttributeProvider(typeof(IListSource))] + public IList DataSource + { + get + { + IEnumerator e = GetSourceLists().GetEnumerator(); + e.MoveNext(); + return e.Current; + } + set + { + if (value == null) + { + // Clear all current data + SourceLists = new BindingList>(); + NewItemsList = null; + FilterAndSort(); + OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); + return; + } + + if (!(value is ICollection)) + { + // list is not a strongy-type collection. + // Check that items in list are all of type T + foreach (object item in value) + { + if (!(item is T)) + { + throw new ArgumentException(string.Format(Properties.Resources.InvalidListItemType, typeof(T).FullName), "DataSource"); + } + } + } + + SourceLists = new object[] { value }; + NewItemsList = value; + } + } + + private bool ShouldSerializeDataSource() + { + return (SourceLists.Count > 0); + } + + protected override void SourceListsChanged(object sender, ListChangedEventArgs e) + { + if ((SourceLists.Count > 1 && e.ListChangedType == ListChangedType.ItemAdded) || e.ListChangedType == ListChangedType.ItemDeleted) + { + throw new Exception("BindingListView allows strictly one source list."); + } + else + { + base.SourceListsChanged(sender, e); + } + } + } +} diff --git a/BindingListView/BindingListView.csproj b/BindingListView/BindingListView.csproj new file mode 100644 index 00000000..e8f394fa --- /dev/null +++ b/BindingListView/BindingListView.csproj @@ -0,0 +1,87 @@ + + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {75AF36A8-7797-4023-B183-5B63D448420A} + Library + Properties + Equin.ApplicationFramework + Equin.ApplicationFramework.BindingListView + + + + + + + + + + + v4.0 + Client + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + + + + + + + + + + Component + + + Component + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + True + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + \ No newline at end of file diff --git a/BindingListView/BindingListView.nuspec b/BindingListView/BindingListView.nuspec new file mode 100644 index 00000000..dfb1e781 --- /dev/null +++ b/BindingListView/BindingListView.nuspec @@ -0,0 +1,17 @@ + + + + $id$ + $version$ + $title$ + $author$ + https://github.com/waynebloss/BindingListView/blob/master/license.txt + https://github.com/waynebloss/BindingListView + false + $description$ + The BindingListView .NET library provides a type-safe, sortable, filterable, data-bindable view of one or more lists of objects. It is the business objects equivalent of using a DataView on a DataTable in ADO.NET. If you have a list of objects to display on a Windows Forms UI (e.g. in a DataGridView) and want to allow your user to sort and filter, then this is the library to use! + Initial + Copyright 2014 + .net IBindingListView winforms + + \ No newline at end of file diff --git a/BindingListView/CompositeItemFilter.cs b/BindingListView/CompositeItemFilter.cs new file mode 100644 index 00000000..f84954dc --- /dev/null +++ b/BindingListView/CompositeItemFilter.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace Equin.ApplicationFramework +{ + public class CompositeItemFilter : IItemFilter + { + private List> _filters; + + public CompositeItemFilter() + { + _filters = new List>(); + } + + public void AddFilter(IItemFilter filter) + { + _filters.Add(filter); + } + + public void RemoveFilter(IItemFilter filter) + { + _filters.Remove(filter); + } + + public bool Include(T item) + { + foreach (IItemFilter filter in _filters) + { + if (!filter.Include(item)) + { + return false; + } + } + return true; + } + + } +} diff --git a/BindingListView/IItemFilter.cs b/BindingListView/IItemFilter.cs new file mode 100644 index 00000000..5cf7b42f --- /dev/null +++ b/BindingListView/IItemFilter.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Equin.ApplicationFramework +{ + /// + /// Defines a general method to test it an item should be included in a . + /// + /// The type of item to be filtered. + public interface IItemFilter + { + /// + /// Tests if the item should be included. + /// + /// The item to test. + /// True if the item should be included, otherwise false. + bool Include(T item); + } + + /// + /// A dummy filter that is used when no filter is needed. + /// It simply includes any and all items tested. + /// + public class IncludeAllItemFilter : IItemFilter + { + public bool Include(T item) + { + // All items are to be included. + // So always return true. + return true; + } + + public override string ToString() + { + return Properties.Resources.NoFilter; + } + + #region Singleton Accessor + + private static IncludeAllItemFilter _instance; + + /// + /// Gets the singleton instance of . + /// + public static IncludeAllItemFilter Instance + { + get + { + if (_instance == null) + { + _instance = new IncludeAllItemFilter(); + } + return _instance; + } + } + + #endregion + } + + /// + /// A filter that uses a user-defined to test items for inclusion in . + /// + public class PredicateItemFilter : IItemFilter + { + /// + /// Creates a new that uses the specified and default name. + /// + /// The used to test items. + public PredicateItemFilter(Predicate includeDelegate) + : this(includeDelegate, null) + { + // The other constructor is called to do the work. + } + + /// + /// Creates a new that uses the specified . + /// + /// The used to test items. + /// The name used for the ToString() return value. + public PredicateItemFilter(Predicate includeDelegate, string name) + { + // We don't allow a null string. Use the default instead. + _name = name ?? defaultName; + if (includeDelegate != null) + { + _includeDelegate = includeDelegate; + } + else + { + throw new ArgumentNullException("includeDelegate", Properties.Resources.IncludeDelegateCannotBeNull); + } + } + + private Predicate _includeDelegate; + private string _name; + private readonly string defaultName = Properties.Resources.PredicateFilter; + + public bool Include(T item) + { + return _includeDelegate(item); + } + + public override string ToString() + { + return _name; + } + } + + // TODO: Implement this class + /* + public class ExpressionItemFilter : IItemFilter + { + public ExpressionItemFilter(string expression) + { + // TODO: Parse expression into predicate + } + + public bool Include(T item) + { + // TODO: use expression... + return true; + } + } + */ + + // TODO: Implement this class + /* + public class CSharpItemFilter : IItemFilter + { + public CSharpItemFilter(string filterSourceCode) + { + + } + + public bool Include(T item) + { + // TODO: implement this method... + return true; + } + } + */ +} diff --git a/BindingListView/INotifyingEditableObject.cs b/BindingListView/INotifyingEditableObject.cs new file mode 100644 index 00000000..afb63add --- /dev/null +++ b/BindingListView/INotifyingEditableObject.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel; + +namespace Equin.ApplicationFramework +{ + /// + /// Extends by providing events to raise during edit state changes. + /// + internal interface INotifyingEditableObject : IEditableObject + { + /// + /// An edit has started on the object. + /// + /// + /// This event should be raised from BeginEdit(). + /// + event EventHandler EditBegun; + /// + /// The editing of the object was cancelled. + /// + /// + /// This event should be raised from CancelEdit(). + /// + event EventHandler EditCancelled; + /// + /// The editing of the object was ended. + /// + /// + /// This event should be raised from EndEdit(). + /// + event EventHandler EditEnded; + } +} diff --git a/BindingListView/InvalidSourceListException.cs b/BindingListView/InvalidSourceListException.cs new file mode 100644 index 00000000..f7c7c4d9 --- /dev/null +++ b/BindingListView/InvalidSourceListException.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Equin.ApplicationFramework +{ + [Serializable] + public class InvalidSourceListException : Exception + { + public InvalidSourceListException() + : base(Properties.Resources.InvalidSourceList) + { + + } + + public InvalidSourceListException(string message) + : base(message) + { + + } + + public InvalidSourceListException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) + : base(info, context) + { + + } + } +} diff --git a/BindingListView/MultiSourceIndexList.cs b/BindingListView/MultiSourceIndexList.cs new file mode 100644 index 00000000..4f3c5720 --- /dev/null +++ b/BindingListView/MultiSourceIndexList.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Collections; + +namespace Equin.ApplicationFramework +{ + internal class MultiSourceIndexList : List, int>> + { + public void Add(IList sourceList, ObjectView item, int index) + { + Add(new KeyValuePair, int>(new ListItemPair(sourceList, item), index)); + } + + /// + /// Searches for a given source index value, returning the list index of the value. + /// + /// The source index to find. + /// Returns the index in this list of the source index, or -1 if not found. + public int IndexOfSourceIndex(IList sourceList, int sourceIndex) + { + for (int i = 0; i < Count; i++) + { + if (this[i].Key.List == sourceList && this[i].Value == sourceIndex) + { + return i; + } + } + return -1; + } + + /// + /// Searches for a given item, returning the index of the value in this list. + /// + /// The item to search for. + /// Returns the index in this list of the item, or -1 if not found. + public int IndexOfItem(T item) + { + for (int i = 0; i < Count; i++) + { + if (this[i].Key.Item.Object.Equals(item) && this[i].Value > -1) + { + return i; + } + } + return -1; + } + + /// + /// Searches for a given item's wrapper, returning the index of the value in this list. + /// + /// The to search for. + /// Returns the index in this list of the item, or -1 if not found. + public int IndexOfKey(ObjectView item) + { + for (int i = 0; i < Count; i++) + { + if (this[i].Key.Item.Equals(item) && this[i].Value > -1) + { + return i; + } + } + return -1; + } + + /// + /// Checks if the list contains a given item. + /// + /// The item to check for. + /// True if the item is contained in the list, otherwise false. + public bool ContainsItem(T item) + { + return (IndexOfItem(item) != -1); + } + + /// + /// Checks if the list contains a given key. + /// + /// The key to search for. + /// True if the key is contained in the list, otherwise false. + public bool ContainsKey(ObjectView key) + { + return (IndexOfKey(key) != -1); + } + + /// + /// Returns an array of all the keys in the list. + /// + public ObjectView[] Keys + { + get + { + return ConvertAll>(new Converter, int>, ObjectView>( + delegate(KeyValuePair, int> kvp) + { return kvp.Key.Item; } + )).ToArray(); + } + } + + /// + /// Returns an to iterate over all the keys in this list. + /// + /// The to use. + public IEnumerator> GetKeyEnumerator() + { + foreach (KeyValuePair, int> kvp in this) + { + yield return kvp.Key.Item; + } + } + } + + internal class ListItemPair + { + private IList _list; + private ObjectView _item; + + public ListItemPair(IList list, ObjectView item) + { + _list = list; + _item = item; + } + + public IList List + { + get + { + return _list; + } + } + + public ObjectView Item + { + get + { + return _item; + } + } + } +} diff --git a/BindingListView/ObjectView.cs b/BindingListView/ObjectView.cs new file mode 100644 index 00000000..fcf203df --- /dev/null +++ b/BindingListView/ObjectView.cs @@ -0,0 +1,456 @@ +using System; +using System.ComponentModel; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace Equin.ApplicationFramework +{ + /// + /// Serves a wrapper for items being viewed in a . + /// This class implements so will raise the necessary events during + /// the item edit life-cycle. + /// + /// + /// If implements this class will call BeginEdit/CancelEdit/EndEdit on the object as well. + /// If implements this class will use that implementation as its own. + /// + /// The type of object being viewed. + [Serializable] + public class ObjectView : INotifyingEditableObject, IDataErrorInfo, INotifyPropertyChanged, ICustomTypeDescriptor, IProvideViews + { + /// + /// Creates a new wrapper for a object. + /// + /// The object being wrapped. + public ObjectView(T @object, AggregateBindingListView parent) + { + _parent = parent; + + Object = @object; + if (Object is INotifyPropertyChanged) + { + ((INotifyPropertyChanged)Object).PropertyChanged += new PropertyChangedEventHandler(ObjectPropertyChanged); + } + + if (typeof(ICustomTypeDescriptor).IsAssignableFrom(typeof(T))) + { + _isCustomTypeDescriptor = true; + _customTypeDescriptor = Object as ICustomTypeDescriptor; + Debug.Assert(_customTypeDescriptor != null); + } + + _providedViews = new Dictionary(); + CreateProvidedViews(); + } + + /// + /// The view containing this ObjectView. + /// + private AggregateBindingListView _parent; + /// + /// Flag that signals if we are currently editing the object. + /// + private bool _editing; + /// + /// The actual object being edited. + /// + private T _object; + /// + /// Flag set to true if type of T implements ICustomTypeDescriptor + /// + private bool _isCustomTypeDescriptor; + /// + /// Holds the Object pre-casted ICustomTypeDescriptor (if supported). + /// + private ICustomTypeDescriptor _customTypeDescriptor; + /// + /// A collection of BindingListView objects, indexed by name, for views auto-provided for any generic IList members. + /// + private Dictionary _providedViews; + + /// + /// Gets the object being edited. + /// + public T Object + { + get { return _object; } + private set + { + if (value == null) + { + throw new ArgumentNullException("Object", Properties.Resources.ObjectCannotBeNull); + } + _object = value; + } + } + + public object GetProvidedView(string name) + { + return _providedViews[name]; + } + + /// + /// Casts an ObjectView<T> to a T by getting the wrapped T object. + /// + /// The ObjectView<T> to cast to a T + /// The object that is wrapped. + public static explicit operator T(ObjectView eo) + { + return eo.Object; + } + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + if (obj is T) + { + return Object.Equals(obj); + } + else if (obj is ObjectView) + { + return Object.Equals((obj as ObjectView).Object); + } + else + { + return Equals(obj); + } + } + + public override int GetHashCode() + { + return Object.GetHashCode(); + } + + public override string ToString() + { + return Object.ToString(); + } + + private void ObjectPropertyChanged(object sender, PropertyChangedEventArgs e) + { + // Raise our own event + OnPropertyChanged(sender, new PropertyChangedEventArgs(e.PropertyName)); + } + + private bool ShouldProvideViewOf(PropertyDescriptor listProp) + { + return _parent.ShouldProvideView(listProp); + } + + private string GetProvidedViewName(PropertyDescriptor listProp) + { + return _parent.GetProvidedViewName(listProp); + } + + private void CreateProvidedViews() + { + foreach (PropertyDescriptor prop in (this as ICustomTypeDescriptor).GetProperties()) + { + if (ShouldProvideViewOf(prop)) + { + object view = _parent.CreateProvidedView(this, prop); + string viewName = GetProvidedViewName(prop); + _providedViews.Add(viewName, view); + } + } + } + + #region INotifyEditableObject Members + + /// + /// Indicates an edit has just begun. + /// + [field: NonSerialized] + public event EventHandler EditBegun; + + /// + /// Indicates the edit was cancelled. + /// + [field: NonSerialized] + public event EventHandler EditCancelled; + + /// + /// Indicated the edit was ended. + /// + [field: NonSerialized] + public event EventHandler EditEnded; + + protected virtual void OnEditBegun() + { + if (EditBegun != null) + { + EditBegun(this, EventArgs.Empty); + } + } + + protected virtual void OnEditCancelled() + { + if (EditCancelled != null) + { + EditCancelled(this, EventArgs.Empty); + } + } + + protected virtual void OnEditEnded() + { + if (EditEnded != null) + { + EditEnded(this, EventArgs.Empty); + } + } + + #endregion + + #region IEditableObject Members + + public void BeginEdit() + { + // As per documentation, this method may get called multiple times for a single edit. + // So we set a flag to only honor the first call. + if (!_editing) + { + _editing = true; + + // If possible call the object's BeginEdit() method + // to let it do what ever it needs e.g. save state + if (Object is IEditableObject) + { + ((IEditableObject)Object).BeginEdit(); + } + // Raise the EditBegun event. + OnEditBegun(); + } + } + + public void CancelEdit() + { + // We can only cancel if currently editing + if (_editing) + { + // If possible call the object's CancelEdit() method + // to let it do what ever it needs e.g. rollback state + if (Object is IEditableObject) + { + ((IEditableObject)Object).CancelEdit(); + } + // Raise the EditCancelled event. + OnEditCancelled(); + // No longer editing now. + _editing = false; + } + } + + public void EndEdit() + { + // We can only end if currently editing + if (_editing) + { + // If possible call the object's EndEdit() method + // to let it do what ever it needs e.g. commit state + if (Object is IEditableObject) + { + ((IEditableObject)Object).EndEdit(); + } + // Raise the EditEnded event. + OnEditEnded(); + // No longer editing now. + _editing = false; + } + } + + #endregion + + #region IDataErrorInfo Members + + // If the wrapped Object support IDataErrorInfo we forward calls to it. + // Otherwise, we just return empty strings that signal "no error". + + string IDataErrorInfo.Error + { + get + { + if (Object is IDataErrorInfo) + { + return ((IDataErrorInfo)Object).Error; + } + return string.Empty; + } + } + + string IDataErrorInfo.this[string columnName] + { + get + { + if (Object is IDataErrorInfo) + { + return ((IDataErrorInfo)Object)[columnName]; + } + return string.Empty; + } + } + + #endregion + + #region INotifyPropertyChanged Members + + [field: NonSerialized] + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (PropertyChanged != null) + { + PropertyChanged(sender, args); + } + } + + #endregion + + #region ICustomTypeDescriptor Members + + AttributeCollection ICustomTypeDescriptor.GetAttributes() + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetAttributes(); + } + else + { + return TypeDescriptor.GetAttributes(Object); + } + } + + string ICustomTypeDescriptor.GetClassName() + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetClassName(); + } + else + { + return typeof(T).FullName; + } + } + + string ICustomTypeDescriptor.GetComponentName() + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetComponentName(); + } + else + { + return TypeDescriptor.GetFullComponentName(Object); + } + } + + TypeConverter ICustomTypeDescriptor.GetConverter() + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetConverter(); + } + else + { + return TypeDescriptor.GetConverter(Object); + } + } + + EventDescriptor ICustomTypeDescriptor.GetDefaultEvent() + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetDefaultEvent(); + } + else + { + return TypeDescriptor.GetDefaultEvent(Object); + } + } + + PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty() + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetDefaultProperty(); + } + else + { + return TypeDescriptor.GetDefaultProperty(Object); + } + } + + object ICustomTypeDescriptor.GetEditor(Type editorBaseType) + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetEditor(editorBaseType); + } + else + { + return null; + } + } + + EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes) + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetEvents(); + } + else + { + return TypeDescriptor.GetEvents(Object, attributes); + } + } + + EventDescriptorCollection ICustomTypeDescriptor.GetEvents() + { + return (this as ICustomTypeDescriptor).GetEvents(null); + } + + PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes) + { + List props; + if (_isCustomTypeDescriptor) + { + props = new List(_parent.AddProvidedViews(_customTypeDescriptor.GetProperties(attributes))); + } + else + { + props = new List(_parent.AddProvidedViews(TypeDescriptor.GetProperties(Object, attributes))); + } + return new PropertyDescriptorCollection(props.ToArray()); + } + + PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() + { + return (this as ICustomTypeDescriptor).GetProperties(null); + } + + object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd) + { + if (_isCustomTypeDescriptor) + { + return _customTypeDescriptor.GetPropertyOwner(pd); + } + else + { + return Object; + } + } + + #endregion + } + + public interface IProvideViews + { + object GetProvidedView(string name); + } + +} diff --git a/BindingListView/Properties/AssemblyInfo.cs b/BindingListView/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..36d30314 --- /dev/null +++ b/BindingListView/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BindingListView")] +[assembly: AssemblyDescription("Advanced, data bindable, object list view.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Equin")] +[assembly: AssemblyProduct("BindingListView")] +[assembly: AssemblyCopyright("Copyright © Andrew Davey 2005")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM componenets. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("709a6d28-582d-423b-ae59-83e0945c4227")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.4.*")] +[assembly: AssemblyFileVersion("1.4.1.0")] + +[assembly: CLSCompliant(true)] \ No newline at end of file diff --git a/BindingListView/Properties/Resources.Designer.cs b/BindingListView/Properties/Resources.Designer.cs new file mode 100644 index 00000000..fbf83b49 --- /dev/null +++ b/BindingListView/Properties/Resources.Designer.cs @@ -0,0 +1,225 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Equin.ApplicationFramework.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Equin.ApplicationFramework.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Cannot add an external item to the view. Use AddNew() or add to source list instead.. + /// + internal static string CannotAddItem { + get { + return ResourceManager.GetString("CannotAddItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot add a new item due to no object being provided in the AddingNew event and a lack of default public constructor.. + /// + internal static string CannotAddNewItem { + get { + return ResourceManager.GetString("CannotAddNewItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot add a new item when NewItemsList is null.. + /// + internal static string CannotAddWhenNewItemsListNull { + get { + return ResourceManager.GetString("CannotAddWhenNewItemsListNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot clear this view.. + /// + internal static string CannotClearView { + get { + return ResourceManager.GetString("CannotClearView", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot insert an external item into this collection.. + /// + internal static string CannotInsertItem { + get { + return ResourceManager.GetString("CannotInsertItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot set an item in the view.. + /// + internal static string CannotSetItem { + get { + return ResourceManager.GetString("CannotSetItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to includeDelegate cannot be null.. + /// + internal static string IncludeDelegateCannotBeNull { + get { + return ResourceManager.GetString("IncludeDelegateCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item in list is not of type {0}.. + /// + internal static string InvalidListItemType { + get { + return ResourceManager.GetString("InvalidListItemType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Source list does not implement IList.. + /// + internal static string InvalidSourceList { + get { + return ResourceManager.GetString("InvalidSourceList", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item was not of type {0}.. + /// + internal static string ItemTypeIncorrect { + get { + return ResourceManager.GetString("ItemTypeIncorrect", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to (no filter). + /// + internal static string NoFilter { + get { + return ResourceManager.GetString("NoFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Object cannot be null.. + /// + internal static string ObjectCannotBeNull { + get { + return ResourceManager.GetString("ObjectCannotBeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to (predicate filter). + /// + internal static string PredicateFilter { + get { + return ResourceManager.GetString("PredicateFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property {0} does not exist in type {1}.. + /// + internal static string PropertyNotFound { + get { + return ResourceManager.GetString("PropertyNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Source list already added to the view.. + /// + internal static string SourceListAlreadyAdded { + get { + return ResourceManager.GetString("SourceListAlreadyAdded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Source list is not in the view.. + /// + internal static string SourceListNotFound { + get { + return ResourceManager.GetString("SourceListNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SourceLists cannot be null.. + /// + internal static string SourceListsNull { + get { + return ResourceManager.GetString("SourceListsNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Synchronized access to the view is not supported.. + /// + internal static string SyncAccessNotSupported { + get { + return ResourceManager.GetString("SyncAccessNotSupported", resourceCulture); + } + } + } +} diff --git a/BindingListView/Properties/Resources.resx b/BindingListView/Properties/Resources.resx new file mode 100644 index 00000000..e29d3914 --- /dev/null +++ b/BindingListView/Properties/Resources.resx @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot add an external item to the view. Use AddNew() or add to source list instead. + + + Cannot add a new item due to no object being provided in the AddingNew event and a lack of default public constructor. + + + Cannot add a new item when NewItemsList is null. + + + Cannot clear this view. + + + Cannot insert an external item into this collection. + + + Cannot set an item in the view. + + + includeDelegate cannot be null. + + + Item in list is not of type {0}. + + + Source list does not implement IList. + + + Item was not of type {0}. + + + (no filter) + + + Object cannot be null. + + + (predicate filter) + + + Property {0} does not exist in type {1}. + + + Source list already added to the view. + + + Source list is not in the view. + + + SourceLists cannot be null. + + + Synchronized access to the view is not supported. + + \ No newline at end of file diff --git a/BindingListView/Properties/Settings.Designer.cs b/BindingListView/Properties/Settings.Designer.cs new file mode 100644 index 00000000..061f018e --- /dev/null +++ b/BindingListView/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Equin.ApplicationFramework.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/BindingListView/Properties/Settings.settings b/BindingListView/Properties/Settings.settings new file mode 100644 index 00000000..39645652 --- /dev/null +++ b/BindingListView/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/BindingListView/ProvidedViewPropertyDescriptor.cs b/BindingListView/ProvidedViewPropertyDescriptor.cs new file mode 100644 index 00000000..5e97000f --- /dev/null +++ b/BindingListView/ProvidedViewPropertyDescriptor.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Equin.ApplicationFramework +{ + class ProvidedViewPropertyDescriptor : PropertyDescriptor + { + public ProvidedViewPropertyDescriptor(string name, Type propertyType) + : base(name, null) + { + _propertyType = propertyType; + } + + private Type _propertyType; + + public override bool CanResetValue(object component) + { + return false; + } + + public override Type ComponentType + { + get { return typeof(IProvideViews); } + } + + public override object GetValue(object component) + { + if (component is IProvideViews) + { + return (component as IProvideViews).GetProvidedView(Name); + } + + throw new ArgumentException("Type of component is not valid.", "component"); + } + + public override bool IsReadOnly + { + get { return true; } + } + + public override Type PropertyType + { + get { return _propertyType; } + } + + public override void ResetValue(object component) + { + throw new NotSupportedException(); + } + + public override void SetValue(object component, object value) + { + throw new NotSupportedException(); + } + + public override bool ShouldSerializeValue(object component) + { + return false; + } + + /// + /// Gets if a BindingListView can be provided for given property. + /// The property type must implement IList<> i.e. some generic IList. + /// + public static bool CanProvideViewOf(PropertyDescriptor prop) + { + Type propType = prop.PropertyType; + foreach (Type interfaceType in propType.GetInterfaces()) + { + if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition().Equals(typeof(IList<>))) + { + return true; + } + } + return false; + } + } +} diff --git a/DoomLauncher.sln b/DoomLauncher.sln index 386aee8f..52b3338a 100644 --- a/DoomLauncher.sln +++ b/DoomLauncher.sln @@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomLauncherRelease", "Doom EndProject Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup", "Setup\Setup.vdproj", "{28E7A7D3-E32C-4D2F-AA60-7EF483DEB3A9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BindingListView", "BindingListView\BindingListView.csproj", "{75AF36A8-7797-4023-B183-5B63D448420A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,6 +90,18 @@ Global {28E7A7D3-E32C-4D2F-AA60-7EF483DEB3A9}.Release|Any CPU.ActiveCfg = Release {28E7A7D3-E32C-4D2F-AA60-7EF483DEB3A9}.Release|Mixed Platforms.ActiveCfg = Release {28E7A7D3-E32C-4D2F-AA60-7EF483DEB3A9}.Release|x86.ActiveCfg = Release + {75AF36A8-7797-4023-B183-5B63D448420A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Debug|x86.ActiveCfg = Debug|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Debug|x86.Build.0 = Debug|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Release|Any CPU.Build.0 = Release|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Release|x86.ActiveCfg = Release|Any CPU + {75AF36A8-7797-4023-B183-5B63D448420A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DoomLauncher/Controls/GameFileViewControl.cs b/DoomLauncher/Controls/GameFileViewControl.cs index aa4708df..8953c292 100644 --- a/DoomLauncher/Controls/GameFileViewControl.cs +++ b/DoomLauncher/Controls/GameFileViewControl.cs @@ -24,7 +24,7 @@ public partial class GameFileViewControl : UserControl, IGameFileColumnView private readonly Label m_label = new Label(); private readonly Dictionary m_properties = new Dictionary(); - private BindingListView m_datasource; + private BindingListView m_datasource; private bool m_binding = false; public GameFileViewControl() @@ -147,30 +147,23 @@ public IEnumerable DataSource get { if (m_datasource != null) - { - foreach (ObjectView item in m_datasource) - yield return item.Object; - } + return m_datasource; else - { - IGameFile[] source = new IGameFile[] { }; - foreach (var gameFile in source) - yield return gameFile; - } + return new IGameFile[] { }; } set { if (value != null) - SetDataSource(new BindingListView(value.ToList())); + SetDataSource(new BindingListView(value.ToList())); else - SetDataSource(new BindingListView(new GameFile[] { })); + SetDataSource(new BindingListView(new GameFile[] { })); } } private void SetDataSource(object datasource) { m_binding = true; - m_datasource = (BindingListView)datasource; + m_datasource = (BindingListView)datasource; if (m_datasource == null) { @@ -325,7 +318,7 @@ void dgvMain_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) { if (m_datasource != null && m_datasource.Count > e.RowIndex) { - GameFile gameFile = m_datasource[e.RowIndex].Object; + IGameFile gameFile = m_datasource[e.RowIndex].Object; if (!m_properties.ContainsKey(e.ColumnIndex)) m_properties.Add(e.ColumnIndex, gameFile.GetType().GetProperty(dgvMain.Columns[e.ColumnIndex].DataPropertyName)); diff --git a/DoomLauncher/DataSources/GameFile.cs b/DoomLauncher/DataSources/GameFile.cs index 9f47afaf..b550266b 100644 --- a/DoomLauncher/DataSources/GameFile.cs +++ b/DoomLauncher/DataSources/GameFile.cs @@ -60,10 +60,8 @@ public object Clone() public override bool Equals(object obj) { - IGameFile check = obj as IGameFile; - - if (check != null) - return ((IGameFile)obj).FileName == FileName; + if (obj is IGameFile gameFile) + return gameFile.FileName == FileName; return false; } diff --git a/DoomLauncher/DataSources/IdGamesGameFile.cs b/DoomLauncher/DataSources/IdGamesGameFile.cs index cc106abe..12c1bd82 100644 --- a/DoomLauncher/DataSources/IdGamesGameFile.cs +++ b/DoomLauncher/DataSources/IdGamesGameFile.cs @@ -104,8 +104,7 @@ void client_DownloadProgressChanged(object sender, DownloadProgressChangedEventA public override bool Equals(object obj) { - IdGamesGameFile gameFile = obj as IdGamesGameFile; - if (gameFile != null) + if (obj is IdGamesGameFile gameFile) return id == gameFile.id; return false; diff --git a/DoomLauncher/DoomLauncher.csproj b/DoomLauncher/DoomLauncher.csproj index 951f36f9..afe9865f 100644 --- a/DoomLauncher/DoomLauncher.csproj +++ b/DoomLauncher/DoomLauncher.csproj @@ -59,10 +59,6 @@ - - False - ..\Equin.ApplicationFramework.BindingListView.dll - False ..\Newtonsoft.Json.dll @@ -787,6 +783,10 @@ + + {75af36a8-7797-4023-b183-5b63d448420a} + BindingListView + {70a25201-8ea4-48f8-a4a6-ed13adf8823c} CheckBoxComboBox diff --git a/DoomLauncher/TabViews/BasicTabViewCtrl.cs b/DoomLauncher/TabViews/BasicTabViewCtrl.cs index 4f4104ff..3189b9b1 100644 --- a/DoomLauncher/TabViews/BasicTabViewCtrl.cs +++ b/DoomLauncher/TabViews/BasicTabViewCtrl.cs @@ -229,15 +229,10 @@ protected void SetDataSource(IEnumerable gameFiles) } else { - GameFileView.DataSource = gameFiles.Cast().ToList(); + GameFileView.DataSource = gameFiles.ToList(); } } - protected IGameFile FromDataBoundItem(object item) - { - return ((ObjectView)item).Object as IGameFile; - } - public virtual bool IsLocal { get { return true; } } public virtual bool IsEditAllowed { get { return true; } } public virtual bool IsDeleteAllowed { get { return true; } } diff --git a/DoomLauncher/TabViews/IdGamesTabViewCtrl.cs b/DoomLauncher/TabViews/IdGamesTabViewCtrl.cs index 5ba49c0c..9b3e7a36 100644 --- a/DoomLauncher/TabViews/IdGamesTabViewCtrl.cs +++ b/DoomLauncher/TabViews/IdGamesTabViewCtrl.cs @@ -76,7 +76,7 @@ private void UpdateIdGamesViewCompleted(object sender, EventArgs e) if (IdGamesDataSource != null) { - base.SetDataSource(IdGamesDataSource.ToList()); + base.SetDataSource(IdGamesDataSource); } else { diff --git a/Equin.ApplicationFramework.BindingListView.dll b/Equin.ApplicationFramework.BindingListView.dll deleted file mode 100644 index c9696be7..00000000 Binary files a/Equin.ApplicationFramework.BindingListView.dll and /dev/null differ