// Bindable list view.
// 2003 - Ian Griffiths (ian@interact-sw.co.uk)
//
// This code is in the public domain, and has no warranty.


using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;

namespace InteractSw.BindingDemo
{
    /// <summary>
    /// A ListView with complex data binding support.
    /// </summary>
    /// <remarks>
    /// <p>Windows Forms provides a built-in <see cref="ListView"/> control,
    /// which is essentially a wrapper of the standard Win32 list view. While
    /// this is a very powerful control, it does not support complex data
    /// binding. It supports simple binding, as all controls do, but simple
    /// binding only binds a single row of data. The absence of complex
    /// binding (i.e. the ability to bind to whole lists of data) is
    /// disappointing in a class whose main purpose is to display lists of
    /// things.</p>
    ///
    /// <p>This class derives from <see cref="ListView"/> and adds support
    /// for complex binding, through its <see cref="DataSource"/> and
    /// <see cref="DataMember"/> properties. These behave much like the
    /// equivalent properties on the =<see cref="DataGrid"/> control.</p>
    ///
    /// <p>Note that the primary purpose of this control is to illustrate
    /// data binding implementation techniques. It is NOT designed as an
    /// industrial-strength control for use in production code. If you use
    /// this in live systems, you do so at your own risk; it would almost
    /// certainly be a better idea to look at the various professional
    /// bindable grid controls on the market.</p>
    /// </remarks>
    public class BindableListView : System.Windows.Forms.ListView
    {
        /// <summary>
        /// The data source to which this control is bound.
        /// </summary>
        /// <remarks>
        /// <p>To make this control display the contents of a data source, you
        /// should set this property to refer to that data source. The source
        /// should implement either <see cref="IList"/>,
        /// <see cref="IBindingList"/>, or <see cref="IListSource"/>.</p>
        ///
        /// <p>When binding to a list container (i.e. one that implements the
        /// <see cref="IListSource"/> interface, such as <see cref="DataSet"/>)
        /// you must also set the <see cref="DataMember"/> property in order
        /// to identify which particular list you would like to display. You
        /// may also set the <see cref="DataMember"/> property even when
        /// DataSource refers to a list, since <see cref="DataMember"/> can
        /// also be used to navigate relations between lists.</p>
        /// </remarks>
        [Category("Data")]
        [TypeConverter(
            "System.Windows.Forms.Design.DataSourceConverter, System.Design")]
        public object DataSource
        {
            get
            {
                return m_dataSource;
            }
            set
            {
                if (m_dataSource != value)
                {
                    // Must be either a list or a list source
                    if (value != null && !(value is IList) &&
                        !(value is IListSource))
                    {
                        throw new ArgumentException(
                            "Data source must be IList or IListSource");
                    }
                    m_dataSource = value;
                    SetDataBinding();
                    OnDataSourceChanged(EventArgs.Empty);
                }
            }
        }
        private object m_dataSource;

        /// <summary>
        /// Raised when the DataSource property changes.
        /// </summary>
        public event EventHandler DataSourceChanged;

        /// <summary>
        /// Called when the DataSource property changes
        /// </summary>
        /// <param name="e">The EventArgs that will be passed to any handlers
        /// of the DataSourceChanged event.</param>
        protected virtual void OnDataSourceChanged(EventArgs e)
        {
            if (DataSourceChanged != null)
                DataSourceChanged(this, e);
        }

        /// <summary>
        /// Identifies the item or relation within the data source whose
        /// contents should be shown.
        /// </summary>
        /// <remarks>
        /// <p>If the <see cref="DataSource"/> refers to a container of lists
        /// such as a <see cref="DataSet"/>, this property should be used to
        /// indicate which list should be shown.</p>
        /// 
        /// <p>Even when <see cref="DataSource"/> refers to a specific list,
        /// you can still set this property to indicate that a related table
        /// should be shown by specifying a relation name. This will cause
        /// this control to display only those rows in the child table related
        /// to the currently selected row in the parent table.</p>
        /// </remarks>
        [Category("Data")]
        [Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design",
                typeof(System.Drawing.Design.UITypeEditor))]
        public string DataMember
        {
            get
            {
                return m_DataMember;
            }
            set
            {
                if (m_DataMember != value)
                {
                    m_DataMember = value;
                    SetDataBinding();
                    OnDataMemberChanged(EventArgs.Empty);
                }
            }
        }
        private string m_DataMember;

        /// <summary>
        /// Raised when the DataMember property changes.
        ///</summary>
        public event EventHandler DataMemberChanged;

        /// <summary>
        /// Called when the DataMember property changes.
        /// </summary>
        /// <param name="e">The EventArgs that will be passed to any handlers
        /// of the DataMemberChanged event.</param>
        protected virtual void OnDataMemberChanged(EventArgs e)
        {
            if (DataMemberChanged != null)
                DataMemberChanged(this, e);
        }


        /// <summary>
        /// Handles binding context changes
        /// </summary>
        /// <param name="e">The EventArgs that will be passed to any handlers
        /// of the BindingContextChanged event.</param>
        protected override void OnBindingContextChanged(EventArgs e)
        {
            base.OnBindingContextChanged (e);

            // If our binding context changes, we must rebind, since we will
            // have a new currency managers, even if we are still bound to the
            // same data source.
            SetDataBinding();
        }


        /// <summary>
        /// Handles parent binding context changes
        /// </summary>
        /// <param name="e">Unused EventArgs.</param>
        protected override void OnParentBindingContextChanged(EventArgs e)
        {
            base.OnParentBindingContextChanged (e);

            // BindingContext is an ambient property - by default it simply picks
            // up the parent control's context (unless something has explicitly
            // given us our own). So we must respond to changes in our parent's
            // binding context in the same way we would changes to our own
            // binding context.
            SetDataBinding();
        }


        // Attaches the control to a data source.
        private void SetDataBinding()
        {
            // The BindingContext is initially null - in general we will not
            // obtain a BindingContext until we are attached to our parent
            // control. (OnParentBindingContextChanged will be called when
            // that happens, so this method will run again. This means it's
            // OK to ignore this call when we don't yet have a BindingContext.)
            if (BindingContext != null)
            {

                // Obtain the CurrencyManager and (if available) IBindingList
                // for the current data source.
                CurrencyManager currencyManager = null;
                IBindingList bindingList = null;

                if (DataSource != null)
                {
                    currencyManager = (CurrencyManager)
                        BindingContext[DataSource, DataMember];
                    if (currencyManager != null)
                    {
                        bindingList = currencyManager.List as IBindingList;
                    }
                }

                // Now see if anything has changed since we last bound to a source.

                bool reloadMetaData = false;
                bool reloadItems = false;
                if (currencyManager != m_currencyManager)
                {
                    // We have a new CurrencyManager. If we were previously
                    // using another CurrencyManager (i.e. if this is not the
                    // first time we've seen one), we'll have some event
                    // handlers attached to the old one, so first we must
                    // detach those.
                    if (m_currencyManager != null)
                    {
                        currencyManager.MetaDataChanged -=
                            new EventHandler(currencyManager_MetaDataChanged);
                        currencyManager.PositionChanged -=
                            new EventHandler(currencyManager_PositionChanged);
                        currencyManager.ItemChanged -=
                            new ItemChangedEventHandler(currencyManager_ItemChanged);
                    }

                    // Now hook up event handlers to the new CurrencyManager.
                    // This enables us to detect when the currently selected
                    // row changes. It also lets us find out more major changes
                    // such as binding to a different list object (this happens
                    // when binding to related views - each time the currently
                    // selected row in a parent changes, the child list object
                    // is replaced with a new object), or even changes in the
                    // set of properties.
                    m_currencyManager = currencyManager;
                    if (currencyManager != null)
                    {
                        reloadMetaData = true;
                        reloadItems = true;
                        currencyManager.MetaDataChanged +=
                            new EventHandler(currencyManager_MetaDataChanged);
                        currencyManager.PositionChanged +=
                            new EventHandler(currencyManager_PositionChanged);
                        currencyManager.ItemChanged +=
                            new ItemChangedEventHandler(currencyManager_ItemChanged);
                    }
                }

                if (bindingList != m_bindingList)
                {
                    // The IBindingList has changed. If we were previously
                    // bound to an IBindingList, detach the event handler.
                    if (m_bindingList != null)
                    {
                        m_bindingList.ListChanged -=
                            new ListChangedEventHandler(bindingList_ListChanged);
                    }

                    // Now hook up a handler to the new IBindingList - this
                    // will notify us of any changes in the list. (This is
                    // more detailed than the CurrencyManager ItemChanged
                    // event. However, we need both, because the only way we
                    // know when the list is replaced completely is when the
                    // CurrencyManager raises the ItemChanged event.)
                    m_bindingList = bindingList;
                    if (bindingList != null)
                    {
                        reloadItems = true;
                        bindingList.ListChanged +=
                            new ListChangedEventHandler(bindingList_ListChanged);
                    }
                }

                // If a change occurred that means the set of properties may
                // have changed, reload these.
                if (reloadMetaData)
                {
                    LoadColumnsFromSource();
                }

                // If a change occurred that means the set of items to be
                // shown in the list may have changed, reload those.
                if (reloadItems)
                {
                    LoadItemsFromSource();
                }
            }

        }
        private CurrencyManager m_currencyManager;
        private IBindingList m_bindingList;
        private PropertyDescriptorCollection m_properties;


        // Reload the properties, and build column headers for them.

        private void LoadColumnsFromSource()
        {
            // Retrieve and store the PropertyDescriptors. (We always go
            // via PropertyDescriptors when binding, and not the Reflection
            // API - this allows generic data sources to decide at runtime
            // what properties to present.) For data sources that don't opt
            // to have dynamic properties, the PropertyDescriptor mechanism
            // automatically falls back to Reflection under the covers.

            m_properties = m_currencyManager.GetItemProperties();


            // Build new column headers for the ListView.

            ColumnHeader[] headers = new ColumnHeader[m_properties.Count];
            Columns.Clear();
            for (int column = 0; column < m_properties.Count; ++column)
            {
                string columnName = m_properties[column].Name;

                // We set the width to be -2 in order to auto-size the column
                // to the header text. Bizarrely, this only works if we set
                // the width after adding the column. (That's we we're not
                // simply passing -2 to Add. The value passed - 0 in this case
                // - is irrelevant here.)
                Columns.Add(columnName, 0, HorizontalAlignment.Left);
                Columns[column].Width = -2;
            }
            // For some reason we seem to need to go back and set the
            // first column's Width to -2 (auto width) a second time.
            // It doesn't stick first time.
            Columns[0].Width = -2;
        }


        // Reload list items from the data source.

        private void LoadItemsFromSource()
        {
            // Tell the control not to bother redrawing until we're done
            // adding new items - avoids flicker and speeds things up.
            BeginUpdate();

            try
            {
                // We're about to rebuild the list, so get rid of the current
                // items.
                Items.Clear();

                // m_bindingList won't be set if the data source doesn't
                // implement IBindingList, so always ask the CurrencyManager
                // for the IList. (IList is all we need to retrieve the rows.)

                IList items = m_currencyManager.List;

                // Add items to list.
                int nItems = items.Count;
                for (int i = 0; i < nItems; ++i)
                {
                    Items.Add(BuildItemForRow(items[i]));
                }
                int index = m_currencyManager.Position;
                if (index != -1)
                {
                    SetSelectedIndex(index);
                }
            }
            finally
            {
                // In finally block just in case the data source does something
                // nasty to us - it feels like it might be bad to leave the
                // control in a state where we called BeginUpdate without a
                // corresponding EndUpdate.
                EndUpdate();
            }
        }

        // Build a single ListViewItem for a single row from the source. (We
        // need to do this when constructing the original list, but this is
        // also called in the IBindingList.ListChanged event handler when
        // updating individual items.)

        private ListViewItem BuildItemForRow(object row)
        {
            string[] itemText = new string[m_properties.Count];
            for (int column = 0; column < itemText.Length; ++column)
            {
                // Use the PropertyDescriptors to extract the property value -
                // this might be a virtual property.

                itemText[column] = m_properties[column].GetValue(row).ToString();
            }
            return new ListViewItem(itemText);
        }


        // IBindingList ListChanged event handler. Deals with fine-grained
        // changes to list items.

        private void bindingList_ListChanged(object sender,
                                                                        ListChangedEventArgs e)
        {
            switch (e.ListChangedType)
            {
                // Well, usually fine-grained... The whole list has changed
                // utterly, so reload it.

                case ListChangedType.Reset:
                    LoadItemsFromSource();
                    break;


                // A single item has changed, so just rebuild that.

                case ListChangedType.ItemChanged:
                    object changedRow = m_currencyManager.List[e.NewIndex];
                    BeginUpdate();
                    Items[e.NewIndex] = BuildItemForRow(changedRow);
                    EndUpdate();
                    break;


                // A new item has appeared, so add that.

                case ListChangedType.ItemAdded:
                    object newRow = m_currencyManager.List[e.NewIndex];
                    // 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.
                    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 add the new row now!
                        BeginUpdate();
                        Items.Insert(e.NewIndex, BuildItemForRow(newRow));
                        EndUpdate();
                    }
                    break;


                // An item has gone away.

                case ListChangedType.ItemDeleted:
                    if (e.NewIndex < Items.Count)
                    {
                        Items.RemoveAt(e.NewIndex);
                    }
                    break;


                // An item has changed its index.

                case ListChangedType.ItemMoved:
                    BeginUpdate();
                    ListViewItem moving = Items[e.OldIndex];
                    Items.Insert(e.NewIndex, moving);
                    EndUpdate();
                    break;


                // Something has changed in the metadata. (This control is
                // too lazy to deal with this in a fine-grained fashion,
                // mostly because the author has never seen this event
                // occur... So we deal with it the simple way: reload
                // everything.)

                case ListChangedType.PropertyDescriptorAdded:
                case ListChangedType.PropertyDescriptorChanged:
                case ListChangedType.PropertyDescriptorDeleted:
                    LoadColumnsFromSource();
                    LoadItemsFromSource();
                    break;
            }
        }


        // The CurrencyManager calls this if the data source looks
        // different. We just reload everything.

        private void currencyManager_MetaDataChanged(object sender,
                                                     EventArgs e)
        {
            LoadColumnsFromSource();
            LoadItemsFromSource();
        }


        // 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.

        private void currencyManager_PositionChanged(object sender,
                                                     EventArgs e)
        {
            SetSelectedIndex(m_currencyManager.Position);
        }


        // Change the currently-selected item. (I'm sure I'm missing a simpler
        // way of doing this... If anyone knows what it is, please let me
        // know!)

        private void SetSelectedIndex(int index)
        {
            // Avoid recursion - we keep track of when we're already in the
            // middle of changing the index, in case the CurrencyManager
            // decides to call us back as a result of a change already in
            // progress. (Not sure if this will ever actually happen - the
            // OnSelectedIndexChanged method uses the m_changingIndex flag to
            // avoid modifying the CurrencyManager's Position when the change
            // in selection was caused by the CurrencyManager in the first
            // place. But it doesn't hurt to be defensive...)
            if (!m_changingIndex)
            {
                m_changingIndex = true;
                SelectedItems.Clear();
                if (Items.Count > index)
                {
                    ListViewItem item = Items[index];
                    item.Selected = true;
                    item.EnsureVisible();
                }
                m_changingIndex = false;
            }
        }
        private bool m_changingIndex;


        // Called by Windows Forms when the currently selected index of the
        // control changes. This usually happens because the user clicked on
        // the control. In this case we want to notify the CurrencyManager so
        // that any other bound controls will remain in sync. This method will
        // also be called when we changed our index as a result of a
        // notification that originated from the CurrencyManager, and in that
        // case we avoid notifying the CurrencyManager back!

        protected override void OnSelectedIndexChanged(EventArgs e)
        {
            base.OnSelectedIndexChanged (e);

            // Did this originate from us, or was this caused by the
            // CurrencyManager in the first place. If we're sure it was us,
            // and there is actually a selected item (this event is also raised
            // when transitioning to the 'no items selected' state), and we
            // definitely do have a CurrencyManager (i.e. we are actually bound
            // to a data source), then we notify the CurrencyManager.

            if (!m_changingIndex && SelectedIndices.Count > 0 &&
            m_currencyManager != null)
            {
                m_currencyManager.Position = SelectedIndices[0];
            }
        }


        // Called by the CurrencyManager when stuff changes. (Yes I know
        // that's vague, but then so is the official documentation.)
        // At time of writing, the official docs imply that you don't need
        // to handle this event if your source implements IBindingList, since
        // IBindingList.ListChanged provides more details information about the
        // change. However, it's not quite as simple as that: when bound to a
        // related view, the list to which we are bound changes every time the
        // selected index of the parent changes, and to see that happen we
        // either have handle this event, or the CurrentChanged (also from the
        // CurrencyManager). So in practice you need to handle both.
        // It doesn't appear to matter whether you handle CurrentChanged or
        // ItemChanged in order to detect such changes - both are raised when
        // the underlying list changes. However, Mark Boulter sent me some
        // example code (thanks Mark!) that used this one, and he probably
        // knows something I don't about which is likely to work better...
        // So I'm doing what his code does and using this event.
        private void currencyManager_ItemChanged(object sender,
                                                 ItemChangedEventArgs e)
        {
            // An index of -1 seems to be the indication that lots has
            // changed. (I've not found where it says this in the
            // documentation - I got this information from a comment in Mark
            // Boulter's code.) So we always reload all items from the
            // source when this happens.
            if (e.Index == -1)
            {
                // ...but before we reload all items from source, we also look
                // to see if the list we're supposed to bind to has changed
                // since last time, and if it has, reattach our event handlers.

                if (!object.ReferenceEquals(m_bindingList, m_currencyManager.List))
                {
                    m_bindingList.ListChanged -=
                        new ListChangedEventHandler(bindingList_ListChanged);
                    m_bindingList = m_currencyManager.List as IBindingList;
                    if (m_bindingList != null)
                    {
                        m_bindingList.ListChanged +=
                            new ListChangedEventHandler(bindingList_ListChanged);
                    }
                }
                LoadItemsFromSource();                
            }
        }
    }
}

Copyright © 2002-2024, Interact Software Ltd. Please direct all Web site inquiries to webmaster@interact-sw.co.uk