Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

Интерактивный выбор объектов из Active Directory (Стандартный object picker диалог на C#)
Active directory содержит несколько встроенных диалогов, из которых самым "известным" даже рядовым пользователям является object picker dialog. Но к сожалению этот диалог, а также диалог обзора доменов до появления NET можно было реализовать только на C++. Данные обстоятельства вызывают желание восполнить пробел в библиотеке NET Framework, создав пользовательскую компоненту, интегрирующую object picker dialog.
Active directory содержит несколько встроенных диалогов, из которых самым "известным" даже рядовым пользователям является object picker dialog:

Но к сожалению этот диалог, а также диалог обзора доменов до появления NET можно было реализовать только на C++ (Данное утверждение не абсолютно: искусства ради автор в свое время написал ActiveX control "ab initio" на VB 6, но такие "изыски" не применимы в стандартной рутинной работе программиста). Вызовом одного API правда запускается диалог обзора контейнеров, но все равно приходится "бороться" с развитыми структурами и, при желании, с callback-процедурой.

Данные обстоятельства вызывают желание восполнить пробел в библиотеке NET Framework, создав пользовательскую компоненту, интегрирующую object picker dialog. В качестве инструмента реализации выберем C# с перспективой использования unsafe-фрагментов для более эффективной работы со структурой DSOP_INIT_INFO. Нетрудно сообразить, что подобную функциональность мы наблюдаем у FileDialog, PrintDialog и других подобных классов – все они потомки System.Windows.Forms.CommonDialog, наследуем и нашу компоненту от этого класса:

namespace PickerSample
{
   /// <summary>
   /// Active Directory object picker dialog
   /// </summary>
   [System.Drawing.ToolboxBitmap(typeof(ADPicker))]
   public class ADPicker : System.Windows.Forms.CommonDialog
   {
      /// <summary>
      /// Default constructor
      /// </summary>
      public ADPicker()
      {
         InitializeComponent();
         m_MustInit = true;
         m_ReturnFormat = adPickerReturnFormat.Name;
         unsafe
         {
            m_Options.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(m_Options);
            m_Options.cDsScopeInfos = 1;
            m_Options.cAttributesToFetch = 0;
            m_Options.apwzAttributeNames = null;
            m_Scope.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(m_Scope);
            m_Scope.pwzADZPath = null;
            m_Scope.pwzDcName = null;
            m_Scope.hr = null;
            m_Scope.flType = (int)(adPickerScopeType.DSOP_SCOPE_TYPE_TARGET_COMPUTER | 
               adPickerScopeType.DSOP_SCOPE_TYPE_UPLEVEL_JOINED_DOMAIN);
            m_Scope.flScope = (int)adPickerScopeFormat.DSOP_SCOPE_FLAG_STARTING_SCOPE;
            m_Scope.FilterFlags.flDownlevel = 
               unchecked((int)((int)adPickerDownlevelFilter.DSOP_DOWNLEVEL_FILTER_USERS+
               2147483648));
            m_Scope.FilterFlags.Uplevel.flBothModes = 
               (int)adPickerUplevelFilter.DSOP_FILTER_USERS;
            m_Scope.FilterFlags.Uplevel.flMixedModeOnly = 
               (int)adPickerUplevelFilter.DSOP_FILTER_USERS;
            m_Scope.FilterFlags.Uplevel.flNativeModeOnly = 
               (int)adPickerUplevelFilter.DSOP_FILTER_USERS;
            fixed(dsopScopeInitInfo *pdsopScope=&m_Scope) 
               m_Options.aDsScopeInfos = pdsopScope;
         }
         if (!this.DesignMode) m_Clipboard = 
            UnmanagedCode.RegisterClipboardFormat(CFSTR_DSOP_DS_SELECTION_LIST);
   }

В данном фрагменте приведен текст конструктора класса. m_MustInit – закрытое поле булева типа, принимающее true, когда изменяются какие-либо свойства диалога. m_Options – структура DSOP_INIT_INFO, а m_Scope – DSOP_SCOPE_INIT_INFO:

[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
internal unsafe struct dsopInitInfo
{
   public int cbSize;
   public string pwzTargetComputer;
   public int cDsScopeInfos;
   public dsopScopeInitInfo* aDsScopeInfos;
   public uint flOptions;
   public int cAttributesToFetch;
   public short* apwzAttributeNames;
}

[StructLayout(LayoutKind.Sequential)]
internal unsafe struct dsopScopeInitInfo
{
   public int cbSize;
   public uint flType;
   public uint flScope;
   public dsopFilter FilterFlags;
   public short* pwzDcName;
   public short* pwzADZPath;
   public void* hr;
}

Как видно, за счет использования unsafe блоков, мы напрямую присваиваем адрес структуры dsopScopeInitInfo полю aDsScopeInfos. CLR-совместимый вариант структуры выглядел бы так:

[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
internal struct dsopInitInfo
{
   public int cbSize;
   public string pwzTargetComputer;
   public int cDsScopeInfos;
   public IntPtr aDsScopeInfos;
   public int flOptions;
   public int cAttributesToFetch;
   public IntPtr apwzAttributeNames;
}

Поскольку мы не планируем извлекать атрибуты Active directories объектов, изменение последнего поля несущественно, а чтобы проинициализировать aDsScopeInfos теперь пришлось бы копировать участки памяти:

m_Options.adDsScopeInfos = Marshal.AllocCoTaskMem(Marshal.SizeOf(dsopScopeInitInfo));
   Marshal.StructureToPtr(m_Scope,m_Options.adDsScopeInfos,false);

не забыв вызвать в Dispose Marshal.DestroyStructure и освободить память.

Многие поля структур DSOP_INIT_INFO, DSOP_SCOPE_INIT_INFO и входящих в них являются битовыми масками. Соответственно используем тип emun с атрибутом Flags():

 

/// <summary>
/// Filter flags to use for an uplevel scope, regardless of whether it is a mixed or 
/// native mode domain
/// </summary>
[Flags()]
public enum adPickerUplevelFilter
{
   /// <summary>
   /// Includes objects that have the showInAdvancedViewOnly attribute set to true
   /// </summary>
   DSOP_FILTER_INCLUDE_ADVANCED_VIEW = 0x00000001,
   /// <summary>
   /// Includes user objects
   /// </summary>
   DSOP_FILTER_USERS = 0x00000002,
   /// <summary>
   /// Includes group objects with a groupType value having the flag 
   /// GROUP_TYPE_BUILTIN_LOCAL_GROUP
   /// </summary>
   DSOP_FILTER_BUILTIN_GROUPS = 0x00000004,
   /// <summary>
   /// Includes the contents of the WellKnown Security Principals container
   /// </summary>
   DSOP_FILTER_WELL_KNOWN_PRINCIPALS = 0x00000008,
   /// <summary>
   /// Includes distribution list universal groups
   /// </summary>
   DSOP_FILTER_UNIVERSAL_GROUPS_DL = 0x00000010,
   /// <summary>
   /// Includes security enabled universal groups
   /// </summary>
   DSOP_FILTER_UNIVERSAL_GROUPS_SE = 0x00000020,
   /// <summary>
   /// Includes distribution list global groups
   /// </summary>
   DSOP_FILTER_GLOBAL_GROUPS_DL = 0x00000040,
   /// <summary>
   /// Includes security enabled global groups
   /// </summary>
   DSOP_FILTER_GLOBAL_GROUPS_SE = 0x00000080,
   /// <summary>
   /// Includes distribution list domain global groups
   /// </summary>
   DSOP_FILTER_DOMAIN_LOCAL_GROUPS_DL = 0x00000100,
   /// <summary>
   /// Includes security enabled domain local groups
   /// </summary>
   DSOP_FILTER_DOMAIN_LOCAL_GROUPS_SE = 0x00000200,
   /// <summary>
   /// Includes contact objects
   /// </summary>
   DSOP_FILTER_CONTACTS = 0x00000400,
   /// <summary>
   /// Includes computer objects
   /// </summary>
   DSOP_FILTER_COMPUTERS = 0x00000800
}

Это перечисление соответствует DSOP_UPLEVEL_FILTER_FLAGS. Покажем как будет выглядеть свойство объекта ADPicker, возвращающее этот тип:

/// <summary>
/// Flags that indicate the filters to use for an uplevel scope. The global 
/// catalog and Windows® 2000 domains are uplevel scopes, which are scopes that 
/// support the ADSI LDAP provider.
/// </summary>
[System.ComponentModel.Description("Flags that indicate the filters to use for an uplevel scope")]
[System.ComponentModel.DefaultValue(typeof(adPickerUplevelFilter),"DSOP_FILTER_USERS")]
public adPickerUplevelFilter UplevelFilter
{
   get { return (adPickerUplevelFilter)m_Scope.FilterFlags.Uplevel.flBothModes; }
   set
   {
      m_MustInit = true;
      m_Scope.FilterFlags.Uplevel.flBothModes = (int)value;
      m_Scope.FilterFlags.Uplevel.flMixedModeOnly = (int)value;
      m_Scope.FilterFlags.Uplevel.flNativeModeOnly = (int)value;
   }
}

Создадим и другие свойства, основанные на перечислениях: DownlevelFilter, ScopeFormat, ScopeType, а также Options, управляющее внешним видом диалога.

Тип Enum явно (explicit) конвертируется в свой базовый тип и из него, такая же конверсия происходит при позднем связывании перечислений через Reflection, то есть при загрузке класса со свойствами данного типа, мы будем видеть эти свойства как возвращающие int или соответственно другие целочисленные типы. С другой стороны конвертер типа для перечисления (System.ComponentModel.EnumConverter) преобразует его в строку из наименований полей перечисления с разделителями.

Теперь создадим в нашем решении еще один проект с простым Windows приложением для тестирования и поместим диалог на форму. Мы увидим, что все свойства отображаются в дизайнере корректно, как и следовало ожидать для перечислений, кроме DowlevelFilter. Так происходит потому, что наряду с "флаговыми" битами в его значениях всегда включен бит 0x80000000. Из документации узнаем, что конвертер типа Enum выдает строку из констант с разделителями только для битовых флагов и перечислений без FlagsAttribute, в противном случае возвращается числовое представление значения перечисления. Такое поведение может сделать недоступным наш класс другим языкам программирования в NET. В качестве выхода можно либо написать свой TypeConverter либо вычитать или прибавлять значение флага в коде свойства, как мы и делаем в данном примере.

Выбранные пользователем объекты из Active directory будут возвращаться в виде массива свойством ReturnValues:

/// <summary>
/// Return array of strings or AD objects if return type is <see langword="object"/>
/// </summary>
[System.ComponentModel.Browsable(false)]
public object[] ReturnValues
{
   get { return m_dsObject; }
}

а их тип будет определяться свойством ReturnFormat, принимающем значения:

 

 
/// <summary>
/// What information about object the user selected to return
/// </summary>
public enum adPickerReturnFormat
{
   /// <summary>
   /// The object's relative distinguished name (RDN)
   /// </summary>
   Name,
   /// <summary>
   /// The object's ADsPath. The format of this string depends on the flags specified in 
   /// the <see cref="P:PickerSample.ADPicker.ScopeFormat"/> for the scope from which this 
   /// object was selected
   /// </summary>
   ADsPath,
   /// <summary>
   /// Object class attribute
   /// </summary>
   Class,
   /// <summary>
   /// Active directory object itself
   /// </summary>
   Object,
   /// <summary>
   /// The object's userPrincipalName attribute value
   /// </summary>
   UPN
}

(см. структуру DS_SELECTION).

Приступим к реализации абстрактных методов базового класса. Их всего два: Reset и RunDialog. Первый будет просто устанавливать все свойства объекта в их первоначальное значение и интереса не представляет. Основные действия по выводу диалога и обработке значений будут находится в RunDialog

/// <summary>
/// Specifies dialog when user calls <see cref="M:System.Windows.Forms.
/// CommonDialog.ShowDialog"/>
/// </summary>
protected override bool RunDialog(IntPtr hwndOwner)
{
   m_dsObject = null;
   if (m_Picker == null)
   try
   {
      Guid g_Clsid = ADPicker.CLSID;
      Guid g_IID = ADPicker.IID;
      m_Picker = (IDSObjectPicker)UnmanagedCode.CoCreateInstance(
         ref g_Clsid,null,UnmanagedCode.InprocServer|UnmanagedCode.LocalServer,ref g_IID);
   }
   catch (System.Runtime.InteropServices.COMException ex)
   {
      throw new ApplicationException(
         String.Format(ResourcesController.GetMessageString("Err_COMError"),
         "ADPicker",ex.Message),ex);
   }
 
   if (m_MustInit)
   {
      m_Picker.Initialize(ref m_Options);
      m_MustInit = false;
   }
   IDataObject pdo=null;
   try
      {
         m_Picker.InvodeDialog(hwndOwner,ref pdo);
      }
      catch (System.Runtime.InteropServices.COMException ex)
      {
         throw new ApplicationException(
            String.Format(ResourcesController.GetMessageString("Err_COMError"),
            "ADPicker",ex.Message),ex);
      }

Как видно из примера, нам также необходимо будет задекларировать интерфейс IDsObjectPicker из Active directory и стандартный IDataObject. Приведем еще цикл, обрабатывающий значения из буфера обмена:

for (int i=0; i<cItems; i++)
   switch (m_ReturnFormat)
   {
      case adPickerReturnFormat.ADsPath:
         m_dsObject[i] = System.Runtime.InteropServices.Marshal.PtrToStringUni
            (System.Runtime.InteropServices.Marshal.ReadIntPtr(pDSSelList, 12 + i * 24));
         break;
      case adPickerReturnFormat.Class:
         m_dsObject[i] = System.Runtime.InteropServices.Marshal.PtrToStringUni
            (System.Runtime.InteropServices.Marshal.ReadIntPtr(pDSSelList, 16 + i * 24));
         break;
      case adPickerReturnFormat.Name:
         m_dsObject[i] = System.Runtime.InteropServices.Marshal.PtrToStringUni
            (System.Runtime.InteropServices.Marshal.ReadIntPtr(pDSSelList, 8 + i * 24));
         break;
      case adPickerReturnFormat.Object:
         m_dsObject[i] = System.Runtime.InteropServices.Marshal.PtrToStringUni
            (System.Runtime.InteropServices.Marshal.ReadIntPtr(pDSSelList, 12 + i * 24));
         try
         {
            object pADObject;
            Guid tempGuid = UnmanagedCode.IID_IADs;
            UnmanagedCode.ADsOpenObject((string)m_dsObject[i],null,null,
               UnmanagedCode.ADS_AUTHENTICATION_SECURE,
               ref tempGuid,out pADObject);
            m_dsObject[i] = pADObject;
         }
         catch {}
         break;
      case adPickerReturnFormat.UPN:
         m_dsObject[i] = System.Runtime.InteropServices.Marshal.PtrToStringUni
            (System.Runtime.InteropServices.Marshal.ReadIntPtr(pDSSelList, 20 + i * 24));
         break;
   }

Здесь мы напрямую из памяти считываем значения полей структур DS_SELECTION и преобразуем их в строки.

В заключение, осталось исправить одну малоудобную деталь в дизайнерском представлении свойств компоненты. Для перечислений с атрибутом Flags пользователь может выбрать интерактивно из раскрывающегося списка в решетке свойств только одно значение, остальные приходится добавлять вручную. Напишем свой редактор интерфейса:

internal class FlagsUIEditor : UITypeEditor
{
   public FlagsUIEditor() : base() {}

   public override object EditValue(
      System.ComponentModel.ITypeDescriptorContext context, 
      IServiceProvider provider, object value)
   {
      System.Windows.Forms.Design.IWindowsFormsEditorService iDrop = 
         (System.Windows.Forms.Design.IWindowsFormsEditorService)provider.GetService(
         typeof(System.Windows.Forms.Design.IWindowsFormsEditorService));
      FlagsEditorControl fe = new FlagsEditorControl(value);
      iDrop.DropDownControl(fe);
      return Enum.Parse(value.GetType(),fe.Flags);
   }

   public override UITypeEditorEditStyle GetEditStyle(
      System.ComponentModel.ITypeDescriptorContext context)
   {
      return UITypeEditorEditStyle.DropDown;
   }

   public override bool GetPaintValueSupported(
      System.ComponentModel.ITypeDescriptorContext context)
   {
      return false;
   }
}

Свойство FlagsEditorControl Flags возвращает строковое представление перечисления, из которого методом Enum.Parse мы получим его значение, а параметр value метода EditValue будет неявно конвертироваться в int. Таким образом мы сможем использовать один редактор для всех перечислений. Приведем еще конструктор и свойство Flags контрола FlagsEditorControl. Выбирать значения перечислений пользователь будет в CheckedListBox.

internal class FlagsEditorControl : System.Windows.Forms.UserControl
{
   private System.Windows.Forms.CheckedListBox lstFlags;
   private System.Windows.Forms.ToolTip toolTip;
   private System.ComponentModel.IContainer components;
 
   public FlagsEditorControl(object Item)
   {
      // This call is required by the Windows.Forms Form Designer.
      InitializeComponent();
 
      ItemProperty = (int)Item;
      int i = 0;
      Values = Enum.GetValues(Item.GetType());
      foreach (string item in Enum.GetNames(Item.GetType()))
      lstFlags.Items.Add(item,
         !Convert.ToBoolean(((int)Values.GetValue(i) & ItemProperty) - 
         (int)Values.GetValue(i++)));
   }
 
   public string Flags
   {
      get
      {
         string strRet = "0";
         foreach (object item in lstFlags.CheckedItems)
            strRet = String.Concat(strRet=="0" ? "" : strRet,
               strRet=="0" ? "" : ", ",item.ToString());
         return strRet;
      }
   }
}


Может пригодится:


Автор: IgorKissil
Прочитано: 4705
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру