Bind a geodatabase table to a .NET control
ArcDataBinding\TableWrapper.cs
// Copyright 2012 ESRI
// 
// All rights reserved under the copyright laws of the United States
// and applicable international laws, treaties, and conventions.
// 
// You may freely redistribute and use this sample code, with or
// without modification, provided you include the original copyright
// notice and use restrictions.
// 
// See the use restrictions.
// 

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.ComponentModel;
using ESRI.ArcGIS.Geodatabase;

namespace ArcDataBinding
{
  /// <summary>
  /// This class provides a wrapper for an ITable that allows it to be bound to
  /// a .NET control.
  /// </summary>
  /// <remarks>
  /// This class inherits from <see cref="BindingList"/> to provide a default
  /// implementation of a list of objects that can be bound to a .NET control.
  /// For the purposes of this sample, it is easier to use BindingList and add
  /// IRows to it than it is to implement all the interfaces required for a 
  /// bindable list. A more correct implementation would allow direct access to
  /// the wrapped ITable rather than simply adding all of its rows to a list.
  /// The class also implements <see cref="ITypedList"/> to allow a control to
  /// query it for any properties required to correctly display the data in a 
  /// control. Normally properties are determined by using reflection. We want
  /// the individual fields in the given ITable to look like properties of an
  /// IRow. As this is not the case, we need to create a collection of 'fake'
  /// properties with one for each field in the ITable. This is contained in the
  /// fakePropertiesList member and is used by the ITypedList implementation.
  /// </remarks>
  [Guid("5a239147-b06a-49e5-aa1c-e47f81adc10e")]
  [ClassInterface(ClassInterfaceType.None)]
  [ProgId("ArcDataBinding.TableWrapper")]
  public class TableWrapper: BindingList<IRow>, ITypedList
  {
    #region Private Members
    /// <summary>
    /// Reference to the table we are wrapping
    /// </summary>
    private ITable wrappedTable;
    
    /// <summary>
    /// This is a list of <see cref="PropertyDescriptor"/> instances with each one
    /// representing one field of the wrapped ITable.
    /// </summary>
    private List<PropertyDescriptor> fakePropertiesList = new List<PropertyDescriptor>();
    
    /// <summary>
    /// Used to start and stop editing when adding/updating/deleting rows
    /// </summary>
    private IWorkspaceEdit wkspcEdit;
    #endregion Private Members

    #region Construction/Destruction
    /// <summary>
    /// This constructor stores a reference to the wrapped ITable and uses it to
    /// generate a list of properties before adding the ITable's data to the binding
    /// list.
    /// </summary>
    /// <param name="tableToWrap">ITable that we wish to bind to .NET controls</param>
    public TableWrapper(ITable tableToWrap)
    {
      wrappedTable = tableToWrap;
      GenerateFakeProperties();
      AddData();
      wkspcEdit = ((IDataset)wrappedTable).Workspace as IWorkspaceEdit;
      AllowNew = true;
      AllowRemove = true;
    }
    #endregion Construction/Destruction

    #region ITypedList Members

    /// <summary>
    /// Returns the <see cref="T:System.ComponentModel.PropertyDescriptorCollection"></see> 
    /// that represents the properties on each item used to bind data.
    /// </summary>
    /// <param name="listAccessors">An array of <see cref="T:System.ComponentModel.PropertyDescriptor"></see> 
    /// objects to find in the collection as bindable. This can be null.</param>
    /// <returns>
    /// The <see cref="T:System.ComponentModel.PropertyDescriptorCollection"></see> 
    /// that represents the properties on each item used to bind data.
    /// </returns>
    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
      PropertyDescriptorCollection propCollection = null;
      if (null == listAccessors)
      {
        // Return all properties
        propCollection = new PropertyDescriptorCollection(fakePropertiesList.ToArray());
      }
      else
      {
        // Return the requested properties by checking each item in listAccessors
        // to make sure it exists in our property collection.
        List<PropertyDescriptor> tempList = new List<PropertyDescriptor>();
        foreach (PropertyDescriptor curPropDesc in listAccessors)
        {
          if (fakePropertiesList.Contains(curPropDesc))
          {
            tempList.Add(curPropDesc);
          }
        }
        propCollection = new PropertyDescriptorCollection(tempList.ToArray());
      }

      return propCollection;
    }

    /// <summary>
    /// Returns the name of the list.
    /// </summary>
    /// <param name="listAccessors">An array of <see cref="T:System.ComponentModel.PropertyDescriptor"></see> 
    /// objects, the list name for which is returned. This can be null.</param>
    /// <returns>The name of the list.</returns>
    public string GetListName(PropertyDescriptor[] listAccessors)
    {
      return ((IDataset)wrappedTable).Name;
    }

    #endregion ITypedList Members

    public bool UseCVDomains
    {
      set
      {
        foreach (FieldPropertyDescriptor curPropDesc in fakePropertiesList)
        {
          if (curPropDesc.HasCVDomain)
          {
            // Field has a coded value domain so turn the usage of this on or off
            // as requested
            curPropDesc.UseCVDomain = value;
          }
        }
      }
    }

    #region Protected Overrides
    /// <summary>
    /// Raises the <see cref="E:System.ComponentModel.BindingList`1.AddingNew"></see> event.
    /// </summary>
    /// <remarks>
    /// This override sets the NewObject property of the event arguments parameter
    /// to be a new IRow.
    /// </remarks>
    /// <param name="e">An <see cref="T:System.ComponentModel.AddingNewEventArgs"></see> 
    /// that contains the event data.</param>
    protected override void OnAddingNew(AddingNewEventArgs e)
    {
      // Check that we can still add rows, this property could have been changed
      if (AllowNew)
      {
        // Need to create a new IRow
        IRow newRow = wrappedTable.CreateRow();
        e.NewObject = newRow;

        // Loop through fields and set default values
        for (int fieldCount = 0; fieldCount < newRow.Fields.FieldCount; fieldCount++)
        {
          IField curField = newRow.Fields.get_Field(fieldCount);
          if (curField.Editable)
          {
            newRow.set_Value(fieldCount, curField.DefaultValue);
          }
        }

        // Save default values
        bool weStartedEditing = StartEditOp();
        newRow.Store();
        StopEditOp(weStartedEditing);

        base.OnAddingNew(e);
      }
    }

    /// <summary>
    /// Removes the item at the specified index.
    /// </summary>
    /// <remarks>
    /// This override calls the Delete method of the IRow that is being removed
    /// </remarks>
    /// <param name="index">The zero-based index of the item to remove.</param>
    protected override void RemoveItem(int index)
    {
      // Check that we can still delete rows, this property could have been changed
      if (AllowRemove)
      {
        // Get the corresponding IRow
        IRow itemToRemove = Items[index];

        bool weStartedEditing = StartEditOp();

        // Delete the row
        itemToRemove.Delete();

        StopEditOp(weStartedEditing);

        base.RemoveItem(index);
      }
    }
    #endregion Protected Overrides

    #region Private Methods
    /// <summary>
    /// Generates 'fake' properties.
    /// </summary>
    /// <remarks>
    /// We need this method to create a list of properties for each field in the
    /// ITable as an IRow does not have a property for each field.
    /// </remarks>
    private void GenerateFakeProperties()
    {
      // Loop through fields in wrapped table
      for (int fieldCount = 0; fieldCount < wrappedTable.Fields.FieldCount; fieldCount++)
      {
        // Create a new property descriptor to represent the field
        FieldPropertyDescriptor newPropertyDesc = new FieldPropertyDescriptor(
          wrappedTable, wrappedTable.Fields.get_Field(fieldCount).Name, fieldCount);
        fakePropertiesList.Add(newPropertyDesc);
      }

    }

    /// <summary>
    /// Adds the data to the binding list.
    /// </summary>
    /// <remarks>
    /// Note that this is a pretty inefficient way of accessing the data to be
    /// bound to a control. If we implemented each of the interfaces required for
    /// a bindable list rather than using BindingList, we could write code that
    /// only reads rows from the ITable as they need to be displayed rather than
    /// reading all of them.
    /// </remarks>
    private void AddData()
    {
      // Get a search cursor that returns all rows. Note we do not want to recycle
      // the returned IRow otherwise all rows in the bound control will be identical
      // to the last row read...
      ICursor cur = wrappedTable.Search(null, false);
      IRow curRow = cur.NextRow();
      while (null != curRow)
      {
        Add(curRow);
        curRow = cur.NextRow();
      }
    }

    /// <summary>
    /// Starts an edit operation.
    /// </summary>
    /// <remarks>
    /// This method is used to start an edit operation before changing any data.
    /// It checks to see if we are in an edit session or not and starts a new
    /// one if appropriate. If we do start an edit session, the method will return
    /// true to indicate that we started an edit session and should therefore also
    /// stop it.
    /// </remarks>
    /// <returns>True if we started an edit session, false if we didn't</returns>
    private bool StartEditOp()
    {
      bool retVal = false;

      // Check to see if we're editing
      if (!wkspcEdit.IsBeingEdited())
      {
        // Not being edited so start here
        wkspcEdit.StartEditing(false);
        retVal = true;
      }

      // Start operation
      wkspcEdit.StartEditOperation();
      return retVal;
    }

    /// <summary>
    /// Stops the edit operation.
    /// </summary>
    /// <remarks>
    /// This method stops an edit operation started with a call to 
    /// <see cref="StartEditOp"/>. If the weStartedEditing parameter is true, this
    /// method will also end the edit session.
    /// </remarks>
    /// <param name="weStartedEditing">if set to <c>true</c> [we started editing].</param>
    private void StopEditOp(bool weStartedEditing)
    {
      // Stop edit operation
      wkspcEdit.StopEditOperation();

      if (weStartedEditing)
      {
        // We started the edit session so stop it here
        wkspcEdit.StopEditing(true);
      }
    }
    #endregion Private Methods
  }
}