From 2264626078e2aebfe9030400663940c717fb54bd Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 20 Jun 2020 12:12:51 -0400 Subject: [PATCH 1/3] update BindListView to use IGameFile interface --- DoomLauncher/Controls/GameFileViewControl.cs | 21 +++++++------------- 1 file changed, 7 insertions(+), 14 deletions(-) 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)); From faa2df7a7ee87d2056ce4043fb8928b2063359d1 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 20 Jun 2020 12:13:11 -0400 Subject: [PATCH 2/3] refactor --- DoomLauncher/DataSources/GameFile.cs | 6 ++---- DoomLauncher/DataSources/IdGamesGameFile.cs | 3 +-- DoomLauncher/TabViews/BasicTabViewCtrl.cs | 7 +------ DoomLauncher/TabViews/IdGamesTabViewCtrl.cs | 2 +- 4 files changed, 5 insertions(+), 13 deletions(-) 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/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 { From f7295c529ac89695033ea4a9db83ddd9b9095067 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 20 Jun 2020 19:27:27 -0400 Subject: [PATCH 3/3] include BindingListView update source to fix bug that doesn't allow interfaces in BindingListView --- BindingListView/AggregateBindingListView.cs | 2009 +++++++++++++++++ BindingListView/BindingListView.cs | 88 + BindingListView/BindingListView.csproj | 87 + BindingListView/BindingListView.nuspec | 17 + BindingListView/CompositeItemFilter.cs | 38 + BindingListView/IItemFilter.cs | 143 ++ BindingListView/INotifyingEditableObject.cs | 33 + BindingListView/InvalidSourceListException.cs | 28 + BindingListView/MultiSourceIndexList.cs | 140 ++ BindingListView/ObjectView.cs | 456 ++++ BindingListView/Properties/AssemblyInfo.cs | 36 + .../Properties/Resources.Designer.cs | 225 ++ BindingListView/Properties/Resources.resx | 174 ++ .../Properties/Settings.Designer.cs | 26 + BindingListView/Properties/Settings.settings | 7 + .../ProvidedViewPropertyDescriptor.cs | 79 + DoomLauncher.sln | 14 + DoomLauncher/DoomLauncher.csproj | 8 +- ...n.ApplicationFramework.BindingListView.dll | Bin 57344 -> 0 bytes 19 files changed, 3604 insertions(+), 4 deletions(-) create mode 100644 BindingListView/AggregateBindingListView.cs create mode 100644 BindingListView/BindingListView.cs create mode 100644 BindingListView/BindingListView.csproj create mode 100644 BindingListView/BindingListView.nuspec create mode 100644 BindingListView/CompositeItemFilter.cs create mode 100644 BindingListView/IItemFilter.cs create mode 100644 BindingListView/INotifyingEditableObject.cs create mode 100644 BindingListView/InvalidSourceListException.cs create mode 100644 BindingListView/MultiSourceIndexList.cs create mode 100644 BindingListView/ObjectView.cs create mode 100644 BindingListView/Properties/AssemblyInfo.cs create mode 100644 BindingListView/Properties/Resources.Designer.cs create mode 100644 BindingListView/Properties/Resources.resx create mode 100644 BindingListView/Properties/Settings.Designer.cs create mode 100644 BindingListView/Properties/Settings.settings create mode 100644 BindingListView/ProvidedViewPropertyDescriptor.cs delete mode 100644 Equin.ApplicationFramework.BindingListView.dll 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/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/Equin.ApplicationFramework.BindingListView.dll b/Equin.ApplicationFramework.BindingListView.dll deleted file mode 100644 index c9696be748d2f7edb51499306a4f962e14c2d63d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57344 zcmeIb34D~*)jxioXC|{{W3mSZ81^BV1j3?#6xk)pCYxZ<7?J@-LY^>_fFT5lQmfXD zR%x{^=nHLesmrTvapA31XwJ(k`{xmi=KDO>N(9>AfiSWw z{b0WUXw{DnT@cJ}LnVyDoM2AW8_cmT1pT0=4OKFtHR_t{x9V6*k>9!q*AlA~P>brX zSR^95GT+mNO7*K+=TXsKyYyaFx7^B_V6K$3&>eKQ!I{vnYGCAL)bAw@N9rNiS_Ke+ zt09p5E>|20q^t)H6$f)wS@VKTLNlb$hoy z5!jrRTwNpsS0(eBCEObku?lUiX&@rt)OXJxZe_?3(#czuEGySCVx!-8-D13G#)nV zr1t~ubD%BnM*kLYbX59HH~JI^V|pj9ww%JzCiMjkq@dZsUDIX70RsgTxfoY#9TyS( ztBJ4Yo32g{bgRJ>N4F_CW>l;)&5re8b48i~TOr@*WZV&9WkyipXcs{;`}n*a{5L8T&=XpTb z2z+xP#tRgZos7tL6G+iQl%5q~T>?5X*C zrIVAZv}VUvguTVqrA#W#6v(aa#aP94MjsU2yNYWrbUD~YE!3r%qli7UieeZ$0>$Nw zBilGD-Gp5nE^~>2nMG2t3d!%jRDM(TAn43;@{JrPvL><#+OkeC2c7ZSlV&WHS6tR* zD9JV4G*%u$!W7{5#9{b z6ebvBM)yME?VNZp7$hKb#8UK)EVOx&rzF_(7jUtz04VfKcp8-tdaN%3%Pz^8uotM; zCj+vWPp;W<4x;?lm4MN6l`Z%!s4NieV`EnXD|A9+Az4AF00hwyM;>GB+(*Sa3zGtW7G{5a~sqtT?% z2p`G`dN2-o?7{j5M4;!G9UQQB0BGa;GJ=KP%61XdLceY8TM&wKbHh>1J@KkWWTgqXm z+SQs55xM{#u^8*CkcZh=T2gG>z; z;_x&$3zkp&5sUwFXmJw)Sj13;Q{z6J`5jW=;+hd=snV^tba`%NzFF=sUEvq4`$6!V zkzHUM^IJ553YM-YiQEa2ZX#(#r_6w-WR&r#IhJ}o+{n5`&2q2DZe-8Ngl2n1s4`}+ zOl5_h0#AA_Q{5*c-V1}i7zLHN&M}O7tu9bY(2W}vKdguc!$MzyPx^#XP|Z|3*1jq( ze}P}bW>NzM0ZCiBB6%0*`5yp5cj;XJuGcGf>ijWyQ z9Ray)WAkwhh%bmOzAq(?WpkfNptmL;-IEM1b{Ty)_FY$-)68|Nh2V{wlxCaoY7y_jgW0c;k z2RT7?;tX&nA3}IDCI%kIE56~X^2&CkkD(~8>SD|l9M`Qw&sL&@`Ix~gon>ZKb^-%# zOY%Qiq?vq}B_hkSaZMEgr6?TjAt;ZsSy!dkMwAXv%SXU?m+U_Rk?(S1uABr!zbG{i zph1`HNupje@;zp9m0pXX-Qaa{iP?cgUEnsXmiOTr@LU%c@tC?Q*LZxlSdW6*$8Ze{ zTaO`>>&Hm$Ds-0=W>ucmbuHUopKrVg@2l0#v->f@WacDb36URqgUz$scf-XP#~fmPF7< zkBOc-Nun`kl4UEUVfM%L9k8AOC^=?U^urnp0~qD9-lZc=ybfsc0OFCm@M5Y8yLp{kkNJ}w0|ik=VoP|DV^FD|;3_7-R+msU z!2(JuP-4OQ15&<^=5s6ayDalRQefiPstS#*seznfYlM$oesa{YC}`><289L zgs3H%h1E628nw-`W7HJ#7RQuSbWbKite>AW*;hHHxT2cvTIY;hp(At@zAYlN?S}T; zc7+g9e})!fmwV&>~U z^9w|{+}1lBkD^ycpkB=~#?h@X>!?0K(_ez^t_7U$4uYIl?EmBA@G$Sd0$UQ8I# zA_wBK`XhOuGMMgquE;N$o609WKgW}EGju|I*@wM71)67qgxFzf>M7j(PC*%xp}K0;)+w^WujIldClUB1X4 z)BIz>Uj+V_`oYeX?~N4&hr*w5vHlEDa!a_oaF~`R?>L!eHdj0k{ zPUe|qf{=wDwlg#VQ#+Z(og9S)+{d_Be+TIN2NOy<|B1^4v^}=+pD;x@%nl~2Cj1vd z(G^JAbM!GpcOK`IW`|t53y{huv5g3H8USXg6s5GnF4Hi{t8}@PL}hGtaN*9p7<*E5 zLz!PrM+uABPPcGHN(LT_xFBrcv9WG5P+3@#bOYlVgbUfp0O2golPm1uDZm4a=Qb`x zMcd{s5< zl%^?)vJqVdlJ?|w4q$4jL{2DRMze!SG5~cBz02wShhf3ZIqi|93RrGCkaE6o6>I9FP zWYE{n;4CI;wNWtZiqlt+>VBA6I9EK>fgP}UCxD>I1U4$Za4wqPlQ8+F6Dz2!K znn_?|)N!bxm=#%-sGnV;M>tniqRQ2Fc|&&WR~@l)GP0L?_1Aod61IpTb`9JxI+fJ` zn`xe{L}d`H*hz=}b^LhRl-N*|Zbu+wb}(e!@>O(O5;){Fl~sFqoR}6$-^cm@jE#R@ z(D{$YkV;%J_F$}IV?A-Ki-5}G8{2Rv0~y~&BEZ8L*U=u|RKNTN(0p0b}_%M7b=SEAf#ABW^)*ywL(* zG~e46hZXs((-0A;Eb&%(eNlIDpc2tuUzNKwP-&Jxho>XDV*)_wShuJ6d|2$Ed~%7; z|5!;5)S-8RB^CLk2@j&a`Gy0rCL%reAMPX1;)+#hFgXdt9G|O`g{S9EGjay-z)M^> zSy6ec$skxb3JXN1YGeu{xgMVs431|=Pfi8Q_WC-xz;Go_!=;le?LFq1j!SX|K!r{( ztJ;wo;T79UXegDPH4}Vk*|Rv+*4cnn!J=ax45&)dHYN#O)yqR)>N_7!E~kX@~6Y4$y%hxQvPgQa(r9Q!Nr;j;M+P6&{Jg17i57=_r5d; z&7fAF3qfpUl>3#3uolcHQl_rA{OrKGg!AYZtIv!54v^ZeD9K zP{olZCa_#;i7Y{ks;#SZY>98{(zL*3>7YXo12gr&sL}(5+W&KUI1gntl@Y7Do=h<( zF#dn6Dej+M!azdMT*{9Budo@m{FgS^)|$XdsbyeU1V`fm)th*%EYV6p!pU1^o$>0JenB550g`mRMZBJQsj3PB(Pa*Y1I`u+~PVgVp zGYU_6WEBEtk;2hlhMez*xxLbU032BHY6lKT3`#uk@*H{>#sZ8CxNl~-nQVZRNTU)J z&%=Jz(39SElECDreRz6?eWr&Q7JhOb5_A0}uDkrI zEUpNb1?5%q+ypvvlbU;{Xs6%xs1EymUvc22HGX~kSFHnOW3v+&s9Tb&Aq^+T?#K*Q z!`Zqil5vnbFXo(1GduX|veq#+$;O(14M#C#Z!{Ix!K`0lwbmmpr?ME%qGp5w9+!0` zgp0GWABYx+Rlmg1sd=oV8Uobh&8|j!?jBst?p7SmqfK^j0DrnNJjz&)KC2|=A z%S77-UzQOa4+v>NP~{%`CCK zjPQ_grEvR+%dvPsEH>W#qQg`y{LYGy_BlJ5p&s@ z7BU~!VReE#xfQ^=l+&R~+zc-3U(Auh*j=J*!bLI29>)1vSn;cS$Z{(u=yf2R9R|FO zk!=uy(=ibS=VKSpY>+R*HAiU;$N857(+j`v_q_1qJZ;;Vxlpb9_(1OqNU0C>u+}y@ z8J7b+6=9)Zt(g&5bW6dwq)-v*6sd?Wc1yvyq)-v*6cTZ&mI1Qp$ zC`a+NTNJ0}D84$Q{%Uqy4N}!OST6^h#*02=M2j7iR;E~AVm9j{@URetLGnOgVS>P27U`@b zUtz8sky0NDW(R(wwZt95(eJzt~XHQ&kvSxC9mANj|g-6Y~ ziY{vevlUg`(ydY>yQOwTzSdJF-Ps~HGgo(EJS;f%=!)FZld$e=;)traqI-$ajWeWG z;Pj_2pQL#zF5LQc2(AhwZ^dOBhlP;Z5e_$9MUmS$@4uQ6xSKtlw*#@~n_*Sn;bsR@ zI`05*MSU38cQJS3yFA{%lp60(Gdq~>vhL(qOb^V@epHObp2%uO_?SZi$kQFUi%E+R zO8y5zS#mo>hFrl9UDxovu^sq&vY@^<6tB5$Cv{th;ZG=Qw!rX!maA zQiF}L^pr!4{nXK_DZx;r0YDkL(>SB)#LjPiT%SrOVOxD|%#E{Z$? z&S*ICGjuavwyAS9>pP$((e;q$Hs%xNioRRXpl-5-`!KmYI>yB^+^3f zkJLSx)V^C`C+>=6$+vkNd=w4QT7cHvCRDiP9^+g3Hj8_tQfFR|9enm@Pd^waCdH;g~6X5!d!vC$JI4mmNi zw&vFO`k0;j;#ez>2xtTOTR3ed|9^kOxG(?c?(f{(KKQ`imw&qNNn`Fm4&3vTZO`w& zZQ>iZ9_f6)F;wy4m4`0-!4CtvgSf@v3jmYo))NUtBl7 z{dp~RDe~9^1sun3A%5QgeJE&{N$GNg58`?!@U8gG1U?M-d$?lBNB@a%2?n~pGpHUn zr#F*Dtu&1q@vj!47_A2rQTW8@`H0+6FzMEMaD`r1Y%Lc2t`UVYU>z8Fj4&Q?#dAOhcKtsP$lK*On7QcsUXb?I}Tb z$=kSuJuxOzx@Y4>=IzpO(@+UkV-o+Ati@1m3;wazSeuQs9|?>lGt>)Z>Khxw_03_t zKP8rEu5Sv*F+PM_t#IqwSa@SRwy8S2G#Lv|Yiyh!+Y}iYwi01Z0R}aKgqz~YR=kb| z!%BT)wXxADHQXHAq~tZW#W+c`6}DDg9Bbg$*u33Y8>Ko$LsP6if!y(`9jUG{PDh|lu>_Z)n{MLGq8;!zSWQi+32i0zEZ;6FFM&s;k z*kT#96+@#HQ!MfIElsiYtgc$FVYN{%f;QFnAR#+~CRiPBLR*hCN*1WLXh^+TNEM_Y z(vpZZ#v8bhHg9BM!fJs!+PNg*RNk>mRO{~7jt9H&6zee71G;t|JWTX^!$Y|O2LJ}> zEOVS0pm_pU0D34Uv7G|HBs@FKBjEg=c^f#N7WgVC9{P>&{7s<8#gh922578{`9J5n z4boP)X1YAIL1M2KxKH45;hg1Wo~PZM>iR5}`NJ%hV|W-=3%ndKKzDdJw!_EauliX2 z$Xtf+=5m=U^O$}hk14;%W0|MtGbL8IxG+F(6f*tS0+U57=cW>te@x&9r5v7K#&C2w zr#ib_mP;wv0@{KUViQ&Fs=u_b5h0-eUdVzNf zd`jR!f&Ua(Hi*+sA2e!Esj&>r9iT4^iVX_T9)TYq)Mf2P>bc4A6T5v!Wc%6*xW0oErsR8fDJ?QEtZrQI>oVFhGBk*wd<*^DRIR6^{C6 zb$}{Hae2-g#UQm}aV2yq z8R0&ZZ*W`*eL*-wf`#eZf{hhy5PCjy)&i@gq4XWdK>2``0NW>6hj0#~rv$s&xX~C1 z-^l?~wGTZC>|Epl{|nMR1_kV>So%n?Z(zeQ5~IoA z1$$Jmsq_!Q%BwkUI(>q2-sD;Yc$aG#;DfFU0e|LN4S2|PG2m}q9e^JRPl5Y#gh#l) z2sqJwJ>VkumjT~${{V2d=Qs_gd%f2KKINT@A+NeHZYpU(Dyf`@jnpC65m5X^g=n0M ziUtFEi>CqR7uN!o70(3>OL#)@LWJiRFL8CzuZt3Z9}4`Jzz0jVxPo+P-=*RWCq+bUp_w}>fL3*oSE1)@Wm)oG>1HS_}VZ<67t$u05 zI-iH095D?e+w&uu5&o&bBSQb9z<-S3`c4?ZGM)UljeG?B-yB(swEsDB4Z@F){2t(e zk&E!8-OD4NK=`1*-;DeL!haNa=g`r954~719`Inr=b)JhBUsP2EZS~nX9{Ota zc{vNiXnMM$Y`bd?cwSFobAg!{V-V+SaPPvMJvkany7vH^JXFP9?!E^Wze;1*xF3N} z=VFb0-MtT3mtb9Vw|hVO(hgu@$b8p*0NC9c`y()n?ZWxA;mUdeE}_YA0wZ_I&w3fy z_XIm)lw`dHECSmkI%0%@`RK4}q+5;?a1an?NwOMm8ZVgbF@7%0k z02{8cc-FhXMhmu`y1-UZ|4toMLDq_J-T%cX}kwhvf7m0|18oCmW$f;ANJ zR-9l_zzV4hYXQdkdOk)Up8+gPNBoOCE~AuIYHSs-GP*=KpElY&exsaT(dpVe`9@zl zoWe@*Am&GcT|r;*3@`@LKT=r42;-EN^E>Rn$Fm<6J3wQ{JdXgY)Yuu`eZWo^>kw=wUFV%_451x@9Wn0k&cO(CyI_a?FN1R^-KnvV zkFnjF^IG2mV<^3t!j>Du=?%fIplf~mFc#f}5rf<4THl36gq}-bYcWzC7wm|!+cyUz zQ_d*nJmS9}IaE+cuq%9z`yK)IhQ^+O%nCXzSQotr&Pw`7r~9QZX;e}kR%I;Z&%SnG zl^V<=>B++Eyw~W2^siBT5eob_H$qUu{&=-!F~Vv5IB+A<F@`2+Y;*P;jL|bSc1QLdSTc5K>^X3bqlYy1r|jLvc>2D={C#uw)9Eyt zU6v^4xq^*Tm~nN^-Nxy(P-73~yoEcbf1o)J=G<>gq~|quEN8bdiGHRzN98_jOs4mB z+{)bjSebsHu{(2bFs9Hy1Uuq?EBA3=UUv4EK_8ZGPcF8Y1mhsI1=BG_T$Sl%bbENT?& z3OXm>WzME=Y0g-Ft~rOkud$B&LbI0ssj(Y@olU1r;Ieel{lLzlvo-c3U~}mrjr}Tr z4xV3nXEJkcr$6TRH|OEy4vg_gyWE&ZR|&#yEde->wqOv*d4%+-EFUg>J5+f^JTi&wce)4hW2 z^j%)O2CJWk1v^037vEuQp|Z1B$^qJ0{J7CclQni%@gvkp7YNox_Y{8&Y=g$0EdGgc zDP5{D?ho7O293R5JO^4Wnxp0%u6q|%X^iWB8O_!h*Zp#;(-_x%J6)nNuKO2ghsL<> zSJ2(Sc*gr3a`+-Wqp`mT_KwD~N;vMHf*mn}f=#UDbVrPSf^E{+aKRoC?6AMO1w(_u){_{>6NZ)=yHvnU-}4L zL*LTaF2P>Z*e9jef%DHATU2%GE1JtWDEqvDN(DPW+|oN}fM7d)SM`0j6sa&>EeC^+_%wojScEI)O|a>sNmlCC$%h4Qdahx4(u9*$@=q7`bao2dRM)ceJ97-3M`WXc8g&5YV1+LexR{e1bahczZL8QjU5xreXc4?c7HBe zUyThGY_!Hs6KuA|&Jk>-##RfK&{(HnS88mBV7Cjl(^oKXH*$DPFlA?ZXpdk!%Jz&} zn!AUdO2vH}oUaLXfW{5H!}uosOkuw110QpLi+-i!mJECnSotEZ!vWuPtS9zTNMjd( zb1w~57&Q;P+t^E$8fzbTzd3dtO<+Tj5jnt??XM!&7->wkcv^Hs^Xwhz(A3iFSyei;}^4>&BfeTaM- z?9 zXwqUpgVJFatrSX3V3R;++Utb!H9(W@l<++Q9~Ag&fgcETrc(UVhjCuB0Zme2rW>T7 z;$J5`3bL+Dtv3~`w3miUWL^o=4x$@C6q`GWvWQeg$3AHj6Rsmi13!eN8nj`R#w zP4B8;X}baKx~Q~wS2Bg6t(lXBn}mmNe6-7Udz8a_QqZKm2)m@@20bKnrCVq2O3zNp zKH+rQo6}~LR#nR?s3^{sI$2ohFt>`!sd(5MZ&0~FRdQ!uRYF(rWHO!fc@D{nuB0s& zdO~2A170IxRsQQG%+QoG0)rI&4xxWj==VzaVF`1riMis45r2CbN8 zJ<2dz`tX!T~0OFV-Lcr=N7=5v5Ou=Ph$5xh~5`UA$GQdi083Ev{~TY*cA_= zSFsx&M3=koM>w3j8~%!=*!7O2>#*A$Nk7Iew}$%Xec!014(wxVsWSf)qn$3m8n+#( zHevlb7c14P=qp&0UPW&U9A9=FDA!>n3JO-BSJCkDTM)h)D^7;@0KSM7Cd0=7%lkg* zdeYeLdc*xJ;66M~%kWKshlTz-34bK;?*a{zdHe!{0?P#s5;zj@ZTCdLw`dyR9I6$1 zNIbA1@xc0t>Ei^3#LpX&J=;g1JYkj-`?x0SSoZ49)M54o&m~^lhQuShPt(~iyhMlD z^Ba=zgJ_YmEI(Bb9+(v(_S3~tl3`hcjKza*fG>>wjT_9>gDbPz%@@l@XN@#i+g;|3 zgPDHY;M24A60b!D$(rCQbN}Euw8MM_Gv*HS_k(v)jZr*gPF9UEVhHn(8**+|Ik7)3 zB>uY)@jgBz9>EZ?FD{B!yDF=RrVV+4S`mI3vFxo5iJy9}xoF4(S$oa9a~{ZgQ1tdP zy!s~~?5{!-^Pb06LL!QgpXTDtcTGo?VtLz7l5_>^H#2(EMu?KVuG%((^1a|cv z>dxK<`2OH4pDAne5V6N0Di*kfc;ES~md2j+koX+7!}hW~*U;j@FVIlpz3>v6i1qmr zl)Til1aeq1`#3_xzKtbxcTUJPQkR5%^idiSyb!hSpEtmBqsVEZBSR*7qDZ^abtCcG zW2Wo}SU)AkVfy=!OFbcCkMUv39s0O2SvV()-X=?FXA^MTFL|0!)AzEX zsOb)*W#3*1J$fvp4S|G^c>hDhKHDd0^w9l)FBcy2d<6Ira@p?62M_yAIm{kX_F{&_ zUwN+VsSb-~*mCyKt>B3m)6feR7;JlgLXPa~3lV!5H<*tNHN6|mCx@~_e&vSzj+8U??HRv!JQWTn2r>^ppbFC>0AziZ6!p}xJuEB_i}+VIJ~8e{438NNZr#aNNP zD<>20%IU~RgRSCS;?t3LiBCt~m9rCQ2A;nVv4`Vb;`5T%P@ZqQ-<4CApo_~^?s_zG zDfoHRdRIJ5#J-Ubv8R6vu`gr`vF~IHv6o~Eu|EX%RBCrrYH?J~ zY>pD2>l~GHouhJob5u@uj>-wpQR1_lqjJ)7R8D%1%DK)_sn=2BeaJ}E_YHRq;BR~n z%Zb{brFIX?so2BfMf|gj8-JFb{t0?YU;i}GlH1@jp&ao8*1A?z-eR~7J}W9R_}plN zxvFvl!mP0xBT>20Uu&pV+gjP_uQ9H#+)ob^`ySoG^RW0R-3ISSK0z6tFu_yY>3^8m zBl)nL%sou(kE}JmSGm()k22f}_}9vN{Tp0G(TDtTS19^jga=7@cyvF)qY$oljg7v5 z@En9UxE5;8U!x4?M?VCmQRvMA6M#(rtAB;-nkWTA2A>5EHF%Vc!m3}UaR!f&li|he z>z`@x8R1-Lps#<4!9K_kN_vl8Y7nQI;tZEuk?$SuU4z%Amt>mp?#d=j~Z_#|?R$lN05k+2+i z(clThY2_A_ywr29cmsb&p{hsdcT%1$#J;XA#J;}}vDX_`ihi*L<#{h_pU#)hHMhvQ z<`&{}%{OtLwlupQwPs7^Gf}*j44$_w#9q7Gz##6G+Y#2&dV#2!4@IZi|4NcA!L z%iPix+3n^9RTlw{FN*hgAh+W}uJ|5El`IYXU`GS87w_kGu`a)x@V zoTuI@XRF&GIRr1)b{Ao!+U`0ZV;7&W&J+*OHPFlxnB(%QOLJ~Cc!arCG`!p3v&`Mb zlOvYVZi7!TcN=`7x!d5A&fQ4$7G^Haw^dGRTjiwoS!iZh*2|)wbLp&6&*jt@7ma!; z=RxrQ6mb2hcXJLS_Gr#Wu6?6c>C1IZl+)5em?h4IeP_O zl*^QXP3b53#7255_n$LU0H8s z{V{8br`5CF^EJ;~p5J)>=^5g^z`NOdgZC!yF7F?_Rla$?FZy=*Ui7`{JLvQH2m9Cg zxBFlB2LcxbHU+}jle1@J&&j?$`+M0fIoom$rr(|P~LRi7Y0cU&K0e@Ze zAmE1r|0VE5%7a+Nvji_ji)`$Ba&WerkKI5Y8jStNQ0!@SpxK9dlh(&tRTO+w$(X9mLe3gs692Lw5GM37TOgL45V1s4IH z6+91cxx`*8@OgnB3mjU&sTK=dC-8cK4;QQi{S^sQVLif;!WiJZA}--h^M@`xztETS zTH5yogm?D+CE)sg3*C6Mq@SwAYJ_j=cLU(P{dWRB-2Ya<9|(L^;70>8z_#=To7x=!w0R#6!=C=mE0{ESQT*oIRoE2f6To~aR{c9wb zQ-$xU=F)Pw4_e*CeFMCIHOLJMF!AI2c0#~>)DMuqypRRTHrR#__v7F`Gqki9bPr%3 zx(rkw&R2Y(VF%fue;M}S!;F#x%3$nw`_PTJC&))% z0W@*So)7+^fPH8uem?l+`+z| z4$!32Py_7cWiLJf_n_mB8bFgK!8%RsyoUgu3E3u1#p%0=efUVgnUH7F*;EC14vhkw zPc=xp0MNwxX)NF(8V|S_vQ6BjJrS{+08N|;oPqEc0ZqCMIhb@iaxn2W;%R{YgdI7na^kd{>Vz0jx@F(~pq)9K) z3c#1~P?L$fs4f6}6?Zk7SPRwzzK(mAO}t?(2KX~t1Na6`9!+`^=ZGe~h1={+`Z@05 zGU*`ZI1@XEB;c=bXS0d*;wHeO*p-{~A@0R8>CZT2gC`R$Z_>Ze&M+h6a=@8lEwf=8 zCceQ*+ofH0NxR%7?Xp|isML-%F6ddJV!m_y;HnN?+e~ny+8Ms`cCsDeLwX5%~#-G=D)z-=pPUm z5f~eo6qpv66F4`pB5-lQ%+Ad&${wG+Gn;b8=1j?n=d8FX2&hAr+4}Y(F0SityuJTmo7ter2#7{&PcPeD&z7 zPhW8ttHBYtM^&=Q(Rhx=^E6){eJ#+}5`8Vx*S`8XKwrc9I$U2P`dXo{QGKn}*U`8x za8=}>D3j+|jcrY_DKu+CTfDh?T1yL_=&9#h+GpdVv$0K9VqNw0cr)LY&o^Q(;|Bs7 zo8UxQvnCPa+t|BBUR3ibl4vn*#hziUZ>dkj5_DFq)xN3Rz9*h$>8B8!j4bj5ay)4@ zBjUu|Yx(B)*>Zzm?@>08!kUD6P1VLj43JhKp25w*>Yu}fHw5YHDDv9La#0BS!I zmHZU_Rcn34V@blZQ>}7mI%x6?1(!Ol&tyX)-hwA1AbAO?=dkppT6EHk=LOL8u_mP= zMya-&HZurhKNyx4V?X?mBzf?FYGE+q;tiaYvLe(E&z~);Z)%H)BF89o zH`FIvQ%1EErO9mP+L`sO^|KNQD^c6L+9GWaw)S)>kx5&!)mqQxni;bd3R|=rRyU-6 z%58?#ygI(7Eg_wU939DSsp<8}7})}pf%YmUev=0dhPTOGjwK_bB!^TE-KpuvnTE|lquwXpJhx}3xR=~V;zo<*r zZ;UyN8=@Pb!FjRut5DvVad`%&KGA;B7^<`6p%S*n8TAcoW3(pLT9@t?cr2+K&hb(- z4sg3@sm$j<7q7M2ni?0Q_0i8Z$5=UK*HjtiGTSQJ~Gh$Yvm+V-G9sdW!@HBwD$hH?|FBsG|p z!1E^4V{6))IX|{sU7)5|V~T=;IWX3Svu}wAhth3}PLphkZI36U=A61kiqSb4ZCyLE z7i~Lnt#2o3L9-Kcgt@0l&uLd`L2rqv=aA6NQ{4)0%wsg`Gw%t`eLJ;y60 z2%Yn)O>!{Ss0S8sOW)#T-L#rY!hxBoY^1=T*w&pAGi)$Z zyLhs&VQqIsEKX5pV`24KWvc!s3mCmn%P{U9qEDgFdfYn&PF5aPMAjx5mol->lAH(E zRBA>(OJZda>GT>u1reyItigDCP=QlOq{J(BXPC<`OPeGQmd=6EVJWFaQnh+I6B%>n z>H2nOn8DAZl7gwioxr3P3rX}8Sv7o0hEnbYtFfHL+7s84%B+>=UBSnXy`G$nYh9b< z*99zSZfciCaG)oZu9D4;UxLvA^S4fy#!s41H*WVf?yeCPKdn`dnFyRzq7(%!c5)6L zQMgTIXpz9QM54aE#{}tR7gp9ScohQLL@qt$?_gF6aV)^ph=$=UwM@VucM3w7f01Ri zib^w(85Z8RkXnlKDzhhX>6JQ`*R2|3=1j~UF6~j$N~9~3mYX>_rZAHd!}7o|7mv$1Vgbh;AH?KJzK1=4nduMe0;I zHFxQ40dMnExq6CG`O4GNY30!z%UYMYEz#SerOhXe;s~w6cIS9QDou}3)7#=$H}m?P z%f?DbuL7Y0$5ulO-Ld4(Sm36u67!kf7No2xQ-9sIk=2X;y9+P9@UhoF|9f!ZYfdnpWXO8oaZNH)}TRBJFK|gH%Uqum-aq4+Cm!kU7wSc3WgS zVjMy3JG0#tNnY~kZk(}8ugBh;2SM5HQbO&`(*!$OZOF0zb+V+1Hk)jUoh*uwmdBxV zGR71~9eX4(Ro2Bo;(-Q0Sm)`_!C9R!iNi zOW5m*eLkG9^-VH0&Z$qXodN$GEwR)qD6~_|9&+^PueUs)+H)pWcqh(~Nm|gH)`AMB z^`LO239$}l)jkIaB1*2QzLh!NbGxUUX|e$lOzN*fr<_o_aJM>U7)-Y~aW{0QX*23h zUeMf~GOZPF_gK}|iVkFtC3g4aF6DGH^kfCRs7Z;CR&!t{sq)F>%xJ1lChc`b8u_eJ z>{6#BWf0UD(!3Gt`;)s0PFh0CPKat_+o7dM>o}G*pRvR<)jNGe3a?Ev@c*7HyOkZ{ zQl!r)k*!`YgjJ|pZ5?)#YWmM=K_XJDM~6OR8Fk(ClG=g&_vExM*u*|)HA=~vRS`}i zEwk@`hTbkcy^h}-#U3NI9kdZ_K@}D88ShMj`yx zL7!CAX4W*x3+n+}fC%LL?RcJ{?6A<~g-e*WrMejuy)O`vm__j7Ca|4{H*GmL47@gR zaEL!cHfw27%G1zI{$jOJl%#qUVISnEyr#!eqOo>CjntJgS6qE8)+EFJ-kxW&K zSDn-3jaFdQTN+5QTPLM*(Oe3%TO~#4u|-sDx-}H;XbDBITjA4MpeOC0sCm0#(ppbx zya#P}*YpWmPPb`>hSTJfcGD!K*^_8hk0q%ghs{7wLS$cgT_;1KSQs>fjudN^Ww|nO zdzO{>vz$?O1ti9i3<_51yDg^`5m_Lib4izxgJZ{(A%Di)#k_85iq|)%7baN#Cvi^K z#Ahlo{Z7yHWMxO55Mhr-dR(o=XbB}?K;0#}$nRQPW6kY7MK}&-PmH$LF4(u97`Y_g>a3Gi z%FKBEnr1868gEF#Mpm`0S;M=qbZSMzB*Q1{DQAE^MR60MDH17NDuN8*i(*anmq;j? z7N-+38=_Xlo8qmGYM%Oy8@4^GbJMhZhgSmARzvC>S`OJ|5sZW4c&k3jf}IHDi+(M_kp4e z&PSa1N&O~EnY&Q{tS@X5mMGA7ym0}BATf=!uF)3j)tbd_i*%f0;lUZJfh8G3srh}R zOcM31l{+%H975w+L@G9~zI83lt5-E#(4WCvF^#<^Bnl8D2P-?j?M z;(fa=AI=Uj1lkj319u$0<46UZUYaFrQp!rj)dD6KVUMZ~tHcc*(4Ep-ta%NTqpW!y z8s9FhmYrl~bx4NY67U8y{dO1d5m8l8dN~DH|^I%Zcrermn7v2C27TquB#G26Z zY?Vx8_37gAl3yyE)VIY*D}v8~VhW2l!>0kq6e4Ds$WIg8c=;zpw+vQBR(3Ebc7pZji*8W8CsLy8*zXpb34{VdCjH!bUNRM*1V>+Fa zT><34Z`h9Uu@pmHsM_=(R&5Vrlc&_w)nVU*;kAdNI|XJIWL#Yxzr5BqLN}L?Hp0_RrS2I< z=xAc@jsH`lJ3(AV+oJPv3!P4rSMwHw!xLz7iv%XprYRHZ>R=!6q1K$nL!&)P(By7j`YEf>7tjfMB9DcfiFI{y z_gdq$W~;e$jdlU8G5Z_Ijrwalr@UBQydFhHDrxpJ580UHi<rp@X@03H}AW=Uh%O(ios=g9=DUYczQ&Sr10 zn%dSk3oA4zD&R$9YUYv{9+a#E-wP@&TyMLeF@WO@^xfQD>+x3W`ZY1oahEDD%z3$z zo~(6HE!ZZ*-o*{Ywmq8XZ{?xIJ(;Iq&$Mo1i=76ic`|2bc&~W49CIqeB~KNfvL{i4 zF?)h0FPSn$P7CZ|WHQRYqZoHUJ&KL{-_Oy`Ej2memF(7fxhtBxGYl8Q7WZfF6ls&O zCn>Z&-iMESADofsdNFKx8e+0v!D*M?a*GY65OfK()FkCs?qS&M7O_~C1Jw<3hcH|Q za>uvaN6g*~`}ODc)ZZ=^)nG4_gnVjulc&A{lB`Dm((Xs{<7sm|$1?+;g75hQ1XLQ& z_*jJfwFX-3{N6$s&+713Cc=0(Ncs~EjPW-W)`QNUx=0|d8BhL%!Fvhb#LSx%{)Ryu zJk5mHq7*8c^D_+;Y!C@8c!r4i6L>YVlM+XIPEF;UHuL)p^~gDf7pSMcpHWDZ(;1`Z2NAXN*Q!wT;``w z=!_FNPW2-xG|-v0hcBdLSGNla4w15-8fkIV1mHA{JK%rqh zad-dv2@ULtEB)yc&hym2dqQX0-;v>7wHkfO9xYTKYle2i(yzj3GycL$*wJ!42A#-{ zZ$C5+xOwqQU!Aifx8SNbmmgV2zVB{cxvYHLJ6Giw7gQO=!5Xu;V5(6_zD(Tkd5Q~s zpyLw81+nv-+^#SgSz*H{SmE*%1ij#z3@*MhH)t31K{z7=j*P0vdJfMs3WBC5FBid> zSx73<9j1by2oCzghNAh9S)Lo2*MyO!Dx@=IF zHY9GBXa$8F?DToVE&~@IvP7O-gMfs#bs;D@hz>|c78gi_>2djxNx{&|o}-(jk*=*RmRX5LU2l zhc67mP5ld&BB?1buV86**bFI!6)g1<7;Y|Dn!`HWb{oWl-xD_Skj5W23%1>vrwU!L zG*1T|l%u*1YjXtgqA$(EWy)qH_d$Y!ZF}&rtBwI8df{~G_kGnm*#L~HlY|pU6Y%xt$LkNZ*AB1L6Ak`mOH1`YB14|T3 zpT(q@nJ4X7NZe$3ct|^C(H=+uF)z=J&?8FZBhrrh1U@0~NmYrPQHg?W&-5?Y_NK-OG z8H9BiJ~jiWO1dgSXaQ{+{Z%MejO2A~8vY1rI%k)X$0owbxXN!bcsq;GRBsA=8;`uB z5O3q5|GX@pkQu)1@=+c-=o~j#=eR+d!OcA^|L>ZmVJaX*s2WnT9sK~snhIL3 z*E!DOIsbw#h~@vRjZi_?iYyPzK@6qq0uHfB^v5s(&J_h+7eEfHp1BLD&{F*I6?t4N#5hn zIoRnzNrM>WF_idxJTODnG;g-gSCH%T1v{Mr)6>(xrMkR^55qAOw6uT7?KS#`pono? zM_2{abf*V2zJ&2go&GKujL(IJ&f`KEc|Je6eR+8p8Q%_ieP95m&&LB2cv0yhC=u;d zTu>$f2r1}l&r=r{R1FO5UN^2tsal+4AqB-M1+^;mB|Yyq~YhW&!2EErb3RJW>J(UzDKs6UKcYQ1#0*N-&1VPGpN2@G|q%1^~C zg{ZkGqZo4@-aLWf^kz0GmkMMR7laH9v&7~mBht+r%13=M%gBEf1DjxbT8eJG)b#kc z-`)h-3nANW;E8T8xqN-l>OL5`P^BN;1QDH&Mdg{)Z~Tbia7w6w79MWT}R`>+*w|i4-E`E6Fb?qhc76^%|Xt6k4mCh zdDyuxKb$3_9jg-mO&N?HK?%B^;MT+?m)9v>Pk@(scol$wLA68IeFa?)7IZzL+QAey z)+$);!xRq#y)Q4=^(;4@N?nM0IunURZRkdNT$1S#sOR;}*9%x@;lkKzmC=n47rM!rIr%M6oY~ zNdt_8j{k(ZJjv4(SOeU7pF!u{_N+udqbS^+w>`^B za8Qu{MM#nFb!7~D(pfoTszGB@zG3avsa|4PP04Ebp{(XLP2w?X#L<+veDn#ZSAX!8 zzlmp>yb?gq`905ddAMd_G~MJ{W!i~ z+}hR{x2lzE3t4M7mhJdY5f~4ycTda!u`E4OnWfFnJ=xpL~=GR~D&eYNo2cnYb8`%4aCbZ;U(Y z;Qw3=4=Dc)l~haQOn`mnP2j}sZ-~Xoiu^s`ZsqwIWlEppoG59b@Qg;i)xU8#ZGjGO z{!odu>MVuNWY`PKUvuKK1NNJ<*PZ>+c*iuLI$;STZ5%O-^O;RdVl9O2FMg*_NZ5y~ zzL5?OF8-o;FPEJkpBy#AgFGKsIS)d7`h0}jq7QiOy6{;_5~bm@s+J5LeEJy=z8%co zjFg;ZkduIK(`0paD zP7Rz@;if>znOmm?dg7X|!56~#`&uW`!uV25812UG!ej_jQqc#JiSHe)j+QXdNt6ifnE*tYM@sGy&CA%K(7XR RHPEYpUJdkW;D22M{|}bbi@yK>