RaUI/Source/ryControls/ObjectListView/Implementation/DataSourceAdapter.cs
鑫Intel 523add43be ### 2021-01-12 dev更新
------
#### ryControls    V2.1.2101.1201
- *.[更新]内置的ObjectListView从1.13更新到2.9.1版本,并对主要属性进行汉化。
- *.[修复]修复新版ObjectListView选中项有筛选结果时,筛选结果白色字体看不清的BUG。
- *.[改进]TextBoxEx2默认事件改为TextChanged2。
2021-01-12 16:32:29 +08:00

627 lines
25 KiB
C#

/*
* DataSourceAdapter - A helper class that translates DataSource events for an ObjectListView
*
* Author: Phillip Piper
* Date: 20/09/2010 7:42 AM
*
* Change log:
* v2.9
* 2015-10-31 JPP - Put back sanity check on upper limit of source items
* 2015-02-02 JPP - Made CreateColumnsFromSource() only rebuild columns when new ones were added
* v2.8.1
* 2014-11-23 JPP - Honour initial CurrencyManager.Position when setting DataSource.
* 2014-10-27 JPP - Fix issue where SelectedObject was not sync'ed with CurrencyManager.Position (SF #129)
* 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 <http://www.gnu.org/licenses/>.
*
* If you wish to use this code in a closed source application, please contact phillip.piper@gmail.com.
*/
using System;
using System.ComponentModel;
using System.Data;
using System.Windows.Forms;
using System.Diagnostics;
namespace BrightIdeasSoftware
{
/// <summary>
/// A helper class that translates DataSource events for an ObjectListView
/// </summary>
public class DataSourceAdapter : IDisposable
{
#region Life and death
/// <summary>
/// Make a DataSourceAdapter
/// </summary>
public DataSourceAdapter(ObjectListView olv) {
if (olv == null) throw new ArgumentNullException("olv");
this.ListView = olv;
// ReSharper disable once DoNotCallOverridableMethodsInConstructor
this.BindListView(this.ListView);
}
/// <summary>
/// Finalize this object
/// </summary>
~DataSourceAdapter() {
this.Dispose(false);
}
/// <summary>
/// Release all the resources used by this instance
/// </summary>
public void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Release all the resources used by this instance
/// </summary>
public virtual void Dispose(bool fromUser) {
this.UnbindListView(this.ListView);
this.UnbindDataSource();
}
#endregion
#region Public Properties
/// <summary>
/// Gets or sets whether or not columns will be automatically generated to show the
/// columns when the DataSource is set.
/// </summary>
/// <remarks>This must be set before the DataSource is set. It has no effect afterwards.</remarks>
public bool AutoGenerateColumns {
get { return this.autoGenerateColumns; }
set { this.autoGenerateColumns = value; }
}
private bool autoGenerateColumns = true;
/// <summary>
/// Get or set the DataSource that will be displayed in this list view.
/// </summary>
public virtual Object DataSource {
get { return dataSource; }
set {
dataSource = value;
this.RebindDataSource(true);
}
}
private Object dataSource;
/// <summary>
/// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data.
/// </summary>
/// <remarks>If the data source is not a DataSet or DataViewManager, this property has no effect</remarks>
public virtual string DataMember {
get { return dataMember; }
set {
if (dataMember != value) {
dataMember = value;
RebindDataSource();
}
}
}
private string dataMember = "";
/// <summary>
/// Gets the ObjectListView upon which this adaptor will operate
/// </summary>
public ObjectListView ListView {
get { return listView; }
internal set { listView = value; }
}
private ObjectListView listView;
#endregion
#region Implementation properties
/// <summary>
/// Gets or sets the currency manager which is handling our binding context
/// </summary>
protected CurrencyManager CurrencyManager {
get { return currencyManager; }
set { currencyManager = value; }
}
private CurrencyManager currencyManager;
#endregion
#region Binding and unbinding
/// <summary>
///
/// </summary>
/// <param name="olv"></param>
protected virtual void BindListView(ObjectListView olv) {
if (olv == null)
return;
olv.Freezing += new EventHandler<FreezeEventArgs>(HandleListViewFreezing);
olv.SelectionChanged += new EventHandler(HandleListViewSelectionChanged);
olv.BindingContextChanged += new EventHandler(HandleListViewBindingContextChanged);
}
/// <summary>
///
/// </summary>
/// <param name="olv"></param>
protected virtual void UnbindListView(ObjectListView olv) {
if (olv == null)
return;
olv.Freezing -= new EventHandler<FreezeEventArgs>(HandleListViewFreezing);
olv.SelectionChanged -= new EventHandler(HandleListViewSelectionChanged);
olv.BindingContextChanged -= new EventHandler(HandleListViewBindingContextChanged);
}
/// <summary>
///
/// </summary>
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);
}
/// <summary>
///
/// </summary>
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
/// <summary>
/// Our data source has changed. Figure out how to handle the new source
/// </summary>
protected virtual void RebindDataSource() {
RebindDataSource(false);
}
/// <summary>
/// Our data source has changed. Figure out how to handle the new source
/// </summary>
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();
}
/// <summary>
/// The data source for this control has changed. Reconfigure the control for the new source
/// </summary>
protected virtual void InitializeDataSource() {
if (this.ListView.Frozen || this.CurrencyManager == null)
return;
this.CreateColumnsFromSource();
this.CreateMissingAspectGettersAndPutters();
this.SetListContents();
this.ListView.AutoSizeColumns();
// Fake a position change event so that the control matches any initial Position
this.HandleCurrencyManagerPositionChanged(null, null);
}
/// <summary>
/// Take the contents of the currently bound list and put them into the control
/// </summary>
protected virtual void SetListContents() {
this.ListView.Objects = this.CurrencyManager.List;
}
/// <summary>
/// Create columns for the listview based on what properties are available in the data source
/// </summary>
/// <remarks>
/// <para>This method will create columns if there is not already a column displaying that property.</para>
/// </remarks>
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;
bool wereColumnsAdded = false;
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);
wereColumnsAdded = true;
}
if (wereColumnsAdded)
generator.PostCreateColumns(this.ListView);
}
/// <summary>
/// Decide if a new column should be added to the control to display
/// the given property
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Configure the given column to show the given property.
/// The title and aspect name of the column are already filled in.
/// </summary>
/// <param name="column"></param>
/// <param name="property"></param>
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();
}
/// <summary>
/// Generate aspect getters and putters for any columns that are missing them (and for which we have
/// enough information to actually generate a getter)
/// </summary>
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
/// <summary>
/// CurrencyManager ListChanged event handler.
/// Deals with fine-grained changes to list items.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="sender"></param>
/// <param name="e"></param>
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 = Stopwatch.StartNew();
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("PERF - Processing {0} event on {1} rows took {2}ms", e.ListChangedType, this.ListView.GetItemCount(), sw.ElapsedMilliseconds));
}
/// <summary>
/// Handle PropertyDescriptor* events
/// </summary>
/// <param name="e"></param>
protected virtual void HandleListChangedMetadataChanged(ListChangedEventArgs e) {
this.InitializeDataSource();
}
/// <summary>
/// Handle ItemMoved event
/// </summary>
/// <param name="e"></param>
protected virtual void HandleListChangedItemMoved(ListChangedEventArgs e) {
// When is this actually triggered?
this.InitializeDataSource();
}
/// <summary>
/// Handle the ItemDeleted event
/// </summary>
/// <param name="e"></param>
protected virtual void HandleListChangedItemDeleted(ListChangedEventArgs e) {
this.InitializeDataSource();
}
/// <summary>
/// Handle an ItemAdded event.
/// </summary>
/// <param name="e"></param>
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();
}
}
/// <summary>
/// Handle the Reset event
/// </summary>
/// <param name="e"></param>
protected virtual void HandleListChangedReset(ListChangedEventArgs e) {
// The whole list has changed utterly, so reload it.
this.InitializeDataSource();
}
/// <summary>
/// Handle ItemChanged event. This is triggered when a single item
/// has changed, so just refresh that one item.
/// </summary>
/// <param name="e"></param>
/// <remarks>Even in this simple case, we should probably rebuild the list.
/// For example, the change could put the item into its own new group.</remarks>
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);
}
/// <summary>
/// The CurrencyManager calls this if the data source looks
/// different. We just reload everything.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// CHECK: Do we need this if we are handle ListChanged metadata events?
/// </remarks>
protected virtual void HandleCurrencyManagerMetaDataChanged(object sender, EventArgs e) {
this.InitializeDataSource();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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;
/// <summary>
/// Change the control's position (which is it's currently selected row)
/// to the nth row in the dataset
/// </summary>
/// <param name="index">The index of the row to be selected</param>
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
/// <summary>
/// Handle the selection changing in our ListView.
/// We need to tell our currency manager about the new position.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected virtual void HandleListViewSelectionChanged(object sender, EventArgs e) {
// Prevent recursion
if (this.isChangingIndex)
return;
// Sanity
if (this.CurrencyManager == null)
return;
// If only one item is selected, tell the currency manager which item is selected.
// CurrencyManager can't handle multiple selection so there's nothing we can do
// if more than one row is selected.
if (this.ListView.SelectedIndices.Count != 1)
return;
try {
this.isChangingIndex = true;
// We can't use the selectedIndex directly, since our listview may be sorted and/or filtered
// 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;
}
}
/// <summary>
/// Handle the frozenness of our ListView changing.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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;
/// <summary>
/// Handle a change to the BindingContext of our ListView.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected virtual void HandleListViewBindingContextChanged(object sender, EventArgs e) {
this.RebindDataSource(false);
}
#endregion
}
}