/* * DataSourceAdapter - A helper class that translates DataSource events for an ObjectListView * * Author: Phillip Piper * Date: 20/09/2010 7:42 AM * * Change log: * v2.6 * 2012-08-16 JPP - Unify common column creation functionality with Generator when possible * * 2010-09-20 JPP - Initial version * * Copyright (C) 2010-2014 Phillip Piper * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. */ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing.Design; using System.Globalization; using System.Windows.Forms; using System.Diagnostics; namespace BrightIdeasSoftware { /// /// A helper class that translates DataSource events for an ObjectListView /// public class DataSourceAdapter : IDisposable { #region Life and death /// /// Make a DataSourceAdapter /// public DataSourceAdapter(ObjectListView olv) { if (olv == null) throw new ArgumentNullException("olv"); this.ListView = olv; this.BindListView(this.ListView); } /// /// Finalize this object /// ~DataSourceAdapter() { this.Dispose(false); } /// /// Release all the resources used by this instance /// public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// /// Release all the resources used by this instance /// public virtual void Dispose(bool fromUser) { this.UnbindListView(this.ListView); this.UnbindDataSource(); } #endregion #region Public Properties /// /// Gets or sets whether or not columns will be automatically generated to show the /// columns when the DataSource is set. /// /// This must be set before the DataSource is set. It has no effect afterwards. public bool AutoGenerateColumns { get { return this.autoGenerateColumns; } set { this.autoGenerateColumns = value; } } private bool autoGenerateColumns = true; /// /// Get or set the DataSource that will be displayed in this list view. /// public virtual Object DataSource { get { return dataSource; } set { dataSource = value; this.RebindDataSource(true); } } private Object dataSource; /// /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. /// /// If the data source is not a DataSet or DataViewManager, this property has no effect public virtual string DataMember { get { return dataMember; } set { if (dataMember != value) { dataMember = value; RebindDataSource(); } } } private string dataMember = ""; /// /// Gets the ObjectListView upon which this adaptor will operate /// public ObjectListView ListView { get { return listView; } internal set { listView = value; } } private ObjectListView listView; #endregion #region Implementation properties /// /// Gets or sets the currency manager which is handling our binding context /// protected CurrencyManager CurrencyManager { get { return currencyManager; } set { currencyManager = value; } } private CurrencyManager currencyManager = null; #endregion #region Binding and unbinding /// /// /// /// protected virtual void BindListView(ObjectListView olv) { if (olv == null) return; olv.Freezing += new EventHandler(HandleListViewFreezing); olv.SelectedIndexChanged += new EventHandler(HandleListViewSelectedIndexChanged); olv.BindingContextChanged += new EventHandler(HandleListViewBindingContextChanged); } /// /// /// /// protected virtual void UnbindListView(ObjectListView olv) { if (olv == null) return; olv.Freezing -= new EventHandler(HandleListViewFreezing); olv.SelectedIndexChanged -= new EventHandler(HandleListViewSelectedIndexChanged); olv.BindingContextChanged -= new EventHandler(HandleListViewBindingContextChanged); } /// /// /// protected virtual void BindDataSource() { if (this.CurrencyManager == null) return; this.CurrencyManager.MetaDataChanged += new EventHandler(HandleCurrencyManagerMetaDataChanged); this.CurrencyManager.PositionChanged += new EventHandler(HandleCurrencyManagerPositionChanged); this.CurrencyManager.ListChanged += new ListChangedEventHandler(CurrencyManagerListChanged); } /// /// /// protected virtual void UnbindDataSource() { if (this.CurrencyManager == null) return; this.CurrencyManager.MetaDataChanged -= new EventHandler(HandleCurrencyManagerMetaDataChanged); this.CurrencyManager.PositionChanged -= new EventHandler(HandleCurrencyManagerPositionChanged); this.CurrencyManager.ListChanged -= new ListChangedEventHandler(CurrencyManagerListChanged); } #endregion #region Initialization /// /// Our data source has changed. Figure out how to handle the new source /// protected virtual void RebindDataSource() { RebindDataSource(false); } /// /// Our data source has changed. Figure out how to handle the new source /// protected virtual void RebindDataSource(bool forceDataInitialization) { CurrencyManager tempCurrencyManager = null; if (this.ListView != null && this.ListView.BindingContext != null && this.DataSource != null) { tempCurrencyManager = this.ListView.BindingContext[this.DataSource, this.DataMember] as CurrencyManager; } // Has our currency manager changed? if (this.CurrencyManager != tempCurrencyManager) { this.UnbindDataSource(); this.CurrencyManager = tempCurrencyManager; this.BindDataSource(); // Our currency manager has changed so we have to initialize a new data source forceDataInitialization = true; } if (forceDataInitialization) InitializeDataSource(); } /// /// The data source for this control has changed. Reconfigure the control for the new source /// protected virtual void InitializeDataSource() { if (this.ListView.Frozen || this.CurrencyManager == null) return; this.CreateColumnsFromSource(); this.CreateMissingAspectGettersAndPutters(); this.SetListContents(); this.ListView.AutoSizeColumns(); } /// /// Take the contents of the currently bound list and put them into the control /// protected virtual void SetListContents() { this.ListView.Objects = this.CurrencyManager.List; } /// /// Create columns for the listview based on what properties are available in the data source /// /// /// This method will create columns if there is not already a column displaying that property. /// protected virtual void CreateColumnsFromSource() { if (this.CurrencyManager == null) return; // Don't generate any columns in design mode. If we do, the user will see them, // but the Designer won't know about them and won't persist them, which is very confusing if (this.ListView.IsDesignMode) return; // Don't create columns if we've been told not to if (!this.AutoGenerateColumns) return; // Use a Generator to create columns Generator generator = Generator.Instance as Generator ?? new Generator(); PropertyDescriptorCollection properties = this.CurrencyManager.GetItemProperties(); if (properties.Count == 0) return; foreach (PropertyDescriptor property in properties) { if (!this.ShouldCreateColumn(property)) continue; // Create a column OLVColumn column = generator.MakeColumnFromPropertyDescriptor(property); this.ConfigureColumn(column, property); // Add it to our list this.ListView.AllColumns.Add(column); } generator.PostCreateColumns(this.ListView); } /// /// Decide if a new column should be added to the control to display /// the given property /// /// /// protected virtual bool ShouldCreateColumn(PropertyDescriptor property) { // Is there a column that already shows this property? If so, we don't show it again if (this.ListView.AllColumns.Exists(delegate(OLVColumn x) { return x.AspectName == property.Name; })) return false; // Relationships to other tables turn up as IBindibleLists. Don't make columns to show them. // CHECK: Is this always true? What other things could be here? Constraints? Triggers? if (property.PropertyType == typeof(IBindingList)) return false; // Ignore anything marked with [OLVIgnore] return property.Attributes[typeof(OLVIgnoreAttribute)] == null; } /// /// Configure the given column to show the given property. /// The title and aspect name of the column are already filled in. /// /// /// protected virtual void ConfigureColumn(OLVColumn column, PropertyDescriptor property) { column.LastDisplayIndex = this.ListView.AllColumns.Count; // If our column is a BLOB, it could be an image, so assign a renderer to draw it. // CONSIDER: Is this a common enough case to warrant this code? if (property.PropertyType == typeof(System.Byte[])) column.Renderer = new ImageRenderer(); } /// /// Generate aspect getters and putters for any columns that are missing them (and for which we have /// enough information to actually generate a getter) /// protected virtual void CreateMissingAspectGettersAndPutters() { foreach (OLVColumn x in this.ListView.AllColumns) { OLVColumn column = x; // stack based variable accessible from closures if (column.AspectGetter == null && !String.IsNullOrEmpty(column.AspectName)) { column.AspectGetter = delegate(object row) { // In most cases, rows will be DataRowView objects DataRowView drv = row as DataRowView; if (drv == null) return column.GetAspectByName(row); return (drv.Row.RowState == DataRowState.Detached) ? null : drv[column.AspectName]; }; } if (column.IsEditable && column.AspectPutter == null && !String.IsNullOrEmpty(column.AspectName)) { column.AspectPutter = delegate(object row, object newValue) { // In most cases, rows will be DataRowView objects DataRowView drv = row as DataRowView; if (drv == null) column.PutAspectByName(row, newValue); else { if (drv.Row.RowState != DataRowState.Detached) drv[column.AspectName] = newValue; } }; } } } #endregion #region Event Handlers /// /// CurrencyManager ListChanged event handler. /// Deals with fine-grained changes to list items. /// /// /// It's actually difficult to deal with these changes in a fine-grained manner. /// If our listview is grouped, then any change may make a new group appear or /// an old group disappear. It is rarely enough to simply update the affected row. /// /// /// protected virtual void CurrencyManagerListChanged(object sender, ListChangedEventArgs e) { Debug.Assert(sender == this.CurrencyManager); // Ignore changes make while frozen, since we will do a complete rebuild when we unfreeze if (this.ListView.Frozen) return; //System.Diagnostics.Debug.WriteLine(e.ListChangedType); //Stopwatch sw = new Stopwatch(); //sw.Start(); switch (e.ListChangedType) { case ListChangedType.Reset: this.HandleListChangedReset(e); break; case ListChangedType.ItemChanged: this.HandleListChangedItemChanged(e); break; case ListChangedType.ItemAdded: this.HandleListChangedItemAdded(e); break; // An item has gone away. case ListChangedType.ItemDeleted: this.HandleListChangedItemDeleted(e); break; // An item has changed its index. case ListChangedType.ItemMoved: this.HandleListChangedItemMoved(e); break; // Something has changed in the metadata. // CHECK: When are these events actually fired? case ListChangedType.PropertyDescriptorAdded: case ListChangedType.PropertyDescriptorChanged: case ListChangedType.PropertyDescriptorDeleted: this.HandleListChangedMetadataChanged(e); break; } //sw.Stop(); //System.Diagnostics.Debug.WriteLine(String.Format("Processing {0} event on {1} rows took {2}ms", e.ListChangedType, this.ListView.GetItemCount(), sw.ElapsedMilliseconds)); } /// /// Handle PropertyDescriptor* events /// /// protected virtual void HandleListChangedMetadataChanged(ListChangedEventArgs e) { this.InitializeDataSource(); } /// /// Handle ItemMoved event /// /// protected virtual void HandleListChangedItemMoved(ListChangedEventArgs e) { // When is this actually triggered? this.InitializeDataSource(); } /// /// Handle the ItemDeleted event /// /// protected virtual void HandleListChangedItemDeleted(ListChangedEventArgs e) { this.InitializeDataSource(); } /// /// Handle an ItemAdded event. /// /// protected virtual void HandleListChangedItemAdded(ListChangedEventArgs e) { // We get this event twice if certain grid controls are used to add a new row to a // datatable: once when the editing of a new row begins, and once again when that // editing commits. (If the user cancels the creation of the new row, we never see // the second creation.) We detect this by seeing if this is a view on a row in a // DataTable, and if it is, testing to see if it's a new row under creation. Object newRow = this.CurrencyManager.List[e.NewIndex]; DataRowView drv = newRow as DataRowView; if (drv == null || !drv.IsNew) { // Either we're not dealing with a view on a data table, or this is the commit // notification. Either way, this is the final notification, so we want to // handle the new row now! this.InitializeDataSource(); } } /// /// Handle the Reset event /// /// protected virtual void HandleListChangedReset(ListChangedEventArgs e) { // The whole list has changed utterly, so reload it. this.InitializeDataSource(); } /// /// Handle ItemChanged event. This is triggered when a single item /// has changed, so just refresh that one item. /// /// /// Even in this simple case, we should probably rebuild the list. /// For example, the change could put the item into its own new group. protected virtual void HandleListChangedItemChanged(ListChangedEventArgs e) { // A single item has changed, so just refresh that. //System.Diagnostics.Debug.WriteLine(String.Format("HandleListChangedItemChanged: {0}, {1}", e.NewIndex, e.PropertyDescriptor.Name)); Object changedRow = this.CurrencyManager.List[e.NewIndex]; this.ListView.RefreshObject(changedRow); } /// /// The CurrencyManager calls this if the data source looks /// different. We just reload everything. /// /// /// /// /// CHECK: Do we need this if we are handle ListChanged metadata events? /// protected virtual void HandleCurrencyManagerMetaDataChanged(object sender, EventArgs e) { this.InitializeDataSource(); } /// /// Called by the CurrencyManager when the currently selected item /// changes. We update the ListView selection so that we stay in sync /// with any other controls bound to the same source. /// /// /// protected virtual void HandleCurrencyManagerPositionChanged(object sender, EventArgs e) { int index = this.CurrencyManager.Position; // Make sure the index is sane (-1 pops up from time to time) if (index < 0 || index >= this.ListView.GetItemCount()) return; // Avoid recursion. If we are currently changing the index, don't // start the process again. if (this.isChangingIndex) return; try { this.isChangingIndex = true; this.ChangePosition(index); } finally { this.isChangingIndex = false; } } private bool isChangingIndex = false; /// /// Change the control's position (which is it's currently selected row) /// to the nth row in the dataset /// /// The index of the row to be selected protected virtual void ChangePosition(int index) { // We can't use the index directly, since our listview may be sorted this.ListView.SelectedObject = this.CurrencyManager.List[index]; // THINK: Do we always want to bring it into view? if (this.ListView.SelectedIndices.Count > 0) this.ListView.EnsureVisible(this.ListView.SelectedIndices[0]); } #endregion #region ObjectListView event handlers /// /// Handle the selection changing in our ListView. /// We need to tell our currency manager about the new position. /// /// /// protected virtual void HandleListViewSelectedIndexChanged(object sender, EventArgs e) { // Prevent recursion if (this.isChangingIndex) return; // If we are bound to a data source, and only one item is selected, // tell the currency manager which item is selected. if (this.ListView.SelectedIndices.Count == 1 && this.CurrencyManager != null) { try { this.isChangingIndex = true; // We can't use the selectedIndex directly, since our listview may be sorted. // So we have to find the index of the selected object within the original list. this.CurrencyManager.Position = this.CurrencyManager.List.IndexOf(this.ListView.SelectedObject); } finally { this.isChangingIndex = false; } } } /// /// Handle the frozenness of our ListView changing. /// /// /// protected virtual void HandleListViewFreezing(object sender, FreezeEventArgs e) { if (!alreadyFreezing && e.FreezeLevel == 0) { try { alreadyFreezing = true; this.RebindDataSource(true); } finally { alreadyFreezing = false; } } } private bool alreadyFreezing = false; /// /// Handle a change to the BindingContext of our ListView. /// /// /// protected virtual void HandleListViewBindingContextChanged(object sender, EventArgs e) { this.RebindDataSource(false); } #endregion } }