Timestamper class extension
TimestampClassExtension.cs
// Copyright 2010 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.Runtime.InteropServices;
using ESRI.ArcGIS.ADF.CATIDs;
using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.Geodatabase;
using Timestamper.Properties;

namespace Timestamper
{
  /// <summary>
  /// A feature class extension for timestamping features with creation dates, modification dates, and
  /// the name of the user who created or last modified the feature.
  /// </summary>
  [Guid("31b0b791-3606-4c58-b4d9-940c157dca4c")]
  [ClassInterface(ClassInterfaceType.None)]
  [ProgId("Timestamper.TimestampClassExtension")]
  [ComVisible(true)]
  public class TimestampClassExtension : IClassExtension, IObjectClassExtension, IFeatureClassExtension,
    IObjectClassEvents, IObjectClassInfo
  {
    #region Member Variables
    /// <summary>
    /// Provides a reference to the extension's class.
    /// </summary>
    private IClassHelper classHelper = null;

    /// <summary>
    /// The extension properties.
    /// </summary>
    private IPropertySet extensionProperties = null;

    /// <summary>
    /// The name of the "created" date field.
    /// </summary>
    private String createdFieldName = Resources.DefaultCreatedField;

    /// <summary>
    /// The position of the "created" date field.
    /// </summary>
    private int createdFieldIndex = -1;

    /// <summary>
    /// The name of the "modified" date field.
    /// </summary>
    private String modifiedFieldName = Resources.DefaultModifiedField;

    /// <summary>
    /// The position of the "modified" date field.
    /// </summary>
    private int modifiedFieldIndex = -1;

    /// <summary>
    /// The name of the "user" text field.
    /// </summary>
    private String userFieldName = Resources.DefaultUserField;

    /// <summary>
    /// The position of the "user" text field.
    /// </summary>
    private int userFieldIndex = -1;

    /// <summary>
    /// The length of the "user" text field.
    /// </summary>
    private int userFieldLength = 0;

    /// <summary>
    /// The name of the current user.
    /// </summary>
    private String userName = String.Empty;
    #endregion

    #region IClassExtension Methods
    /// <summary>
    /// Initializes the extension.
    /// </summary>
    /// <param name="classHelper">Provides a reference to the extension's class.</param>
    /// <param name="extensionProperties">A set of properties unique to the extension.</param>
    public void Init(IClassHelper classHelper, IPropertySet extensionProperties)
    {
      // Store the class helper as a member variable.
      this.classHelper = classHelper;
      IClass baseClass = classHelper.Class;

      // Get the names of the created and modified fields, if they exist.
      if (extensionProperties != null)
      {
        this.extensionProperties = extensionProperties;

        object createdObject = extensionProperties.GetProperty(Resources.CreatedFieldKey);
        object modifiedObject = extensionProperties.GetProperty(Resources.ModifiedFieldKey);
        object userObject = extensionProperties.GetProperty(Resources.UserFieldKey);

        // Make sure the properties exist and are strings.
        if (createdObject != null && createdObject is String)
        {
          createdFieldName = Convert.ToString(createdObject);
        }
        if (modifiedObject != null && modifiedObject is String)
        {
          modifiedFieldName = Convert.ToString(modifiedObject);
        }
        if (userObject != null && userObject is String)
        {
          userFieldName = Convert.ToString(userObject);
        }
      }
      else
      {
        // First time the extension has been run. Initialize with default values.
        InitNewExtension();
      }

      // Set the positions of the fields.
      SetFieldIndexes();

      // Set the current user name.
      userName = GetCurrentUser();
    }

    /// <summary>
    /// Informs the extension that the class is being disposed of.
    /// </summary>
    public void Shutdown()
    {
      classHelper = null;
    }
    #endregion

    #region IObjectClassEvents Methods
    /// <summary>
    /// Fired when an object's attributes or geometry is updated.
    /// </summary>
    /// <param name="obj">The updated object.</param>
    public void OnChange(IObject obj)
    {
      // Set the modified field's value to the current date and time.
      if (modifiedFieldIndex != -1)
      {
        obj.set_Value(modifiedFieldIndex, DateTime.Now);

        // Set the user field's value to the current user.
        if (userFieldIndex != -1)
        {
          obj.set_Value(userFieldIndex, userName);
        }
      }
    }

    /// <summary>
    /// Fired when a new object is created.
    /// </summary>
    /// <param name="obj">The new object.</param>
    public void OnCreate(IObject obj)
    {
      // Set the created field's value to the current date and time.
      if (createdFieldIndex != -1)
      {
        obj.set_Value(createdFieldIndex, DateTime.Now);
      }

      // Set the user field's value to the current user.
      if (userFieldIndex != -1)
      {
        obj.set_Value(userFieldIndex, userName);
      }
    }

    /// <summary>
    /// Fired when an object is deleted.
    /// </summary>
    /// <param name="obj">The deleted object.</param>
    public void OnDelete(IObject obj)
    {}
    #endregion

    #region IObjectClassInfo Methods
    /// <summary>
    /// Indicates if updates to objects can bypass the Store method and OnChange notifications for efficiency.
    /// </summary>
    /// <returns>False; this extension requires Store to be called.</returns>
    public Boolean CanBypassStoreMethod()
    {
      return false;
    }
    #endregion

    #region Public Members
    /// <summary>
    /// Changes the member variables and extension properties to store the provided field names
    /// as the created, modified and user fields (positions are also refreshed). Empty strings
    /// indicate the values should not be saved in a field.
    /// </summary>
    /// <param name="createdField">The name of the "created" field.</param>
    /// <param name="modifiedField">The name of the "modified" field.</param>
    /// <param name="userField">The name of the "user" field.</param>
    public void SetTimestampFields(String createdField, String modifiedField, String userField)
    {
      IClass baseClass = classHelper.Class;
      ISchemaLock schemaLock = (ISchemaLock)baseClass;
      try
      {
        // Get an exclusive lock. We want to do this prior to making any changes
        // to ensure the member variables and extension properties remain synchronized.
        schemaLock.ChangeSchemaLock(esriSchemaLock.esriExclusiveSchemaLock);

        // Set the name member variables.
        createdFieldName = createdField;
        modifiedFieldName = modifiedField;
        userFieldName = userField;

        // Set the positions of the fields.
        SetFieldIndexes();

        // Modify the extension properties.
        extensionProperties.SetProperty(Resources.CreatedFieldKey, createdFieldName);
        extensionProperties.SetProperty(Resources.ModifiedFieldKey, modifiedFieldName);
        extensionProperties.SetProperty(Resources.UserFieldKey, userFieldName);

        // Change the properties.
        IClassSchemaEdit2 classSchemaEdit = (IClassSchemaEdit2)baseClass;
        classSchemaEdit.AlterClassExtensionProperties(extensionProperties);
      }
      catch (COMException comExc)
      {
        throw new Exception(Resources.FailedToSavePropertiesMsg, comExc);
      }
      finally
      {
        schemaLock.ChangeSchemaLock(esriSchemaLock.esriSharedSchemaLock);
      }
    }

    /// <summary>
    /// The field storing the creation date of features.
    /// </summary>
    public String CreatedField
    {
      get
      {
        return createdFieldName;
      }
    }

    /// <summary>
    /// The field storing the modification date of features.
    /// </summary>
    public String ModifiedField
    {
      get
      {
        return modifiedFieldName;
      }
    }

    /// <summary>
    /// The field storing the user who created or last modified the feature.
    /// </summary>
    public String UserField
    {
      get
      {
        return userFieldName;
      }
    }
    #endregion

    #region Private Methods
    /// <summary>
    /// This method should be called the first time the extension is initialized, when the
    /// extension properties are null. This will create a new set of properties with the default
    /// field names.
    /// </summary>
    private void InitNewExtension()
    {
      // First time the extension has been run, initialize the extension properties.
      extensionProperties = new PropertySetClass();
      extensionProperties.SetProperty(Resources.CreatedFieldKey, createdFieldName);
      extensionProperties.SetProperty(Resources.ModifiedFieldKey, modifiedFieldName);
      extensionProperties.SetProperty(Resources.UserFieldKey, userFieldName);

      // Store the properties.
      IClass baseClass = classHelper.Class;
      IClassSchemaEdit2 classSchemaEdit = (IClassSchemaEdit2)baseClass;
      classSchemaEdit.AlterClassExtensionProperties(extensionProperties);
    }

    /// <summary>
    /// Gets the name of the extension's user. For local geodatabases, this is the username as known
    /// by the operating system (in a domain\username format). For remote geodatabases, the
    /// IDatabaseConnectionInfo interface is utilized.
    /// </summary>
    /// <returns>The name of the current user.</returns>
    private String GetCurrentUser()
    {
      // Get the base class' workspace.
      IClass baseClass = classHelper.Class;
      IDataset dataset = (IDataset)baseClass;
      IWorkspace workspace = dataset.Workspace;

      // If supported, use the IDatabaseConnectionInfo interface to get the username.
      IDatabaseConnectionInfo databaseConnectionInfo = workspace as IDatabaseConnectionInfo;
      if (databaseConnectionInfo != null)
      {
        String connectedUser = databaseConnectionInfo.ConnectedUser;

        // If the user name is longer than the user field allows, shorten it.
        if (connectedUser.Length > userFieldLength)
        {
          connectedUser = connectedUser.Substring(0, userFieldLength);
        }

        return connectedUser;
      }

      // Get the current Windows user.
      String userDomain = Environment.UserDomainName;
      String userName = Environment.UserName;
      String qualifiedUserName = String.Format(@"{0}\{1}", userDomain, userName);

      // If the user name is longer than the user field allows, shorten it.
      if (qualifiedUserName.Length > userFieldLength)
      {
        qualifiedUserName = qualifiedUserName.Substring(0, userFieldLength);
      }

      return qualifiedUserName;
    }

    /// <summary>
    /// Finds the positions of the created, modified and user fields, and verifies that
    /// the specified field has the correct data type.
    /// </summary>
    private void SetFieldIndexes()
    {
      // Get the base class from the class helper.
      IClass baseClass = classHelper.Class;

      // Find the indexes of the fields.
      createdFieldIndex = baseClass.FindField(createdFieldName);
      modifiedFieldIndex = baseClass.FindField(modifiedFieldName);
      userFieldIndex = baseClass.FindField(userFieldName);

      // Verify that the field data types are correct.
      IFields fields = baseClass.Fields;
      if (createdFieldIndex != -1)
      {
        IField createdField = fields.get_Field(createdFieldIndex);

        // If the "created" field is not a date field, do not use it.
        if (createdField.Type != esriFieldType.esriFieldTypeDate)
        {
          createdFieldIndex = -1;
        }
      }
      if (modifiedFieldIndex != -1)
      {
        IField modifiedField = fields.get_Field(modifiedFieldIndex);

        // If the "modified" field is not a date field, do not use it.
        if (modifiedField.Type != esriFieldType.esriFieldTypeDate)
        {
          modifiedFieldIndex = -1;
        }
      }
      if (userFieldIndex != -1)
      {
        IField userField = fields.get_Field(userFieldIndex);

        // If the "user" field is not a text field, do not use it.
        if (userField.Type != esriFieldType.esriFieldTypeString)
        {
          userFieldIndex = -1;
        }
        else
        {
          // Get the length of the text field.
          userFieldLength = userField.Length;
        }
      }
    }
    #endregion

    #region COM Registration Function(s)
    /// <summary>
    /// Registers the class extension in the appropriate component category.
    /// </summary>
    /// <param name="registerType">The class description's type.</param>
    [ComRegisterFunction()]
    [ComVisible(false)]
    static void RegisterFunction(Type registerType)
    {
      string regKey = string.Format("HKEY_CLASSES_ROOT\\CLSID\\{{{0}}}", registerType.GUID);
      GeoObjectClassExtensions.Register(regKey);
    }

    /// <summary>
    /// Removes the class extension from the appropriate component category.
    /// </summary>
    /// <param name="registerType">The class description's type.</param>
    [ComUnregisterFunction()]
    [ComVisible(false)]
    static void UnregisterFunction(Type registerType)
    {
      string regKey = string.Format("HKEY_CLASSES_ROOT\\CLSID\\{{{0}}}", registerType.GUID);
      GeoObjectClassExtensions.Unregister(regKey);
    }
    #endregion
  }
}