Создание типа поля в SharePoint

Создание типа поля, унаследованного от типа Пользователь (User) с реализацией кастомного пикера (EntityPicker).

Шаг 1. Декларация своего типа поля

Все типы полей, используемые в SharePoint должны быть описаны в XML-файле (в одном или нескольких). Имя этого файла должно начинаться с fldtypes и располагаться он должен в папке %SharePointRoot%\TEMPLATE\XML. Типы полей SharePoint по умолчанию описаны в файле %SharePointRoot%\TEMPLATE\XML\fldtypes.xml.

Первое, что надо сделать, для создания своего типа поля - описать его. Для этого в проект добавим новую папку XML, сопоставленную с папкой SharePoint %SharePointRoot%\TEMPLATE\XML и создадим в ней XML-файл fldtypes_CustomUserField.xml (расширение здесь не важно, т.к. SharePoint при инициализации набора полей считывает все файлы, имена которых начинаются с fldtypes в независимости от их расширения). Так как новый тип поля будет унаследован от типа Пользователь(User), то для простоты просто выборочно воспроизведем описание этого поля по аналогии с родительским типом:

<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
  <FieldType>
    <Field Name="TypeName">CustomUserField</Field>
    <Field Name="ParentType">User</Field>
    <Field Name="TypeDisplayName">Custom UserField</Field>
    <Field Name="TypeShortDescription">Custom UserField</Field>
    <Field Name="UserCreatable">TRUE</Field>
    <Field Name="FieldTypeClass">$FieldClassFullName$</Field>
    <Field Name="Sortable">TRUE</Field>
    <Field Name="Filterable">TRUE</Field>
    <Field Name="ShowInListCreate">TRUE</Field>
    <Field Name="ShowInSurveyCreate">TRUE</Field>
    <Field Name="ShowInDocumentLibraryCreate">TRUE</Field>
    <Field Name="ShowInColumnTemplateCreate">TRUE</Field>
    <Field Name="FieldEditorUserControl">/_controltemplates/UserFieldEditor.ascx</Field>
  </FieldType>
</FieldTypes>

Несколько слов о свойствах нового типа поля:

  • TypeName - текстовый идентификатор типа. Должен быть уникален в пределах фермы;
  • ParentType - указание на родительский тип. В нашем случае - User;
  • TypeDisplayName - имя типа поля, отображаемое в интерфейсе при добавлении нового столбца в список/библиотеку;
  • UserCreatable - показывать (TRUE) или нет (FALSE) тип поля в пользовательском интерфейсе;
  • FieldTypeClass - полное имя класса, описывающее поле;
  • FieldEditorUserControl - контрол, отображаемый на форме добавления столбца в список/библиотеку;

Эти свойства не наследуются от родительского типа, т.е. их значения надо явно указывать. FieldEditorUserControl контрол я указал стандартный. В одном из следующих постов я покажу как можно создавать и использовать свои свойства для типа поля. Описание остальных свойств можно найти на MSDN.

Шаг 2. Класс, описывающий тип поля

Теперь надо создать класс, описывающий новый тип поля, унаследовав его от класса Microsoft.SharePoint.SPFieldUser. Родительский класс SPFieldUser является модифицированной версией lookup-поля:

Для начала изменять функционал поля не будет, только переопределим свойство FieldRenderingControl, возвращающее контрол, инициализация которого необходима для отображения этого поля в интерфейсе пользователя.

Класс получился очень небольшим:

public class CustomUserField : SPFieldUser
{
    public CustomUserField(SPFieldCollection fields, string fieldName)
        : base(fields, fieldName)
    {
        Init();
    }
 
    public CustomUserField(SPFieldCollection fields, string typeName, string displayName)
        : base(fields, typeName, displayName)
    {
        Init();
    }
 
    private void Init()
    {
        Type = SPFieldType.User;
        TypeAsString = "CustomUserField";
    }
 
    public override BaseFieldControl FieldRenderingControl
    {
        get
        {
            BaseFieldControl control;
            if (CountRelated)
            {
                control = new ComputedField();
            }
            else if (AllowMultipleValues)
            {
                control = new CustomUserFieldControlMulti();
            }
            else
            {
                control = new CustomUserFieldControl();
            }
            control.FieldName = InternalName;
            return control;
        }
    }
}

В конструкторе просто указываем тип нашего поля из enum'а SPFieldType (в моем случае SPFieldType.User), а также тип, который указан в XML определении нового типа ("CustomUserField") в поле TypeName.

В свойстве FieldRenderingControl логика предельно проста. Если поле является счетчиком ссылок на элемент (сопутствующий подсчет), то возвращаем стандартный контрол Microsoft.SharePoint.WebControls.ComputedField. В противном случае возвращаем свои контролы для lookup-поля с единственным и множественным значениями. Эти контролы необходимы для отображения своего пикера. В моем случае я создал два соответствующих контрола:

Можно использовать и стандартные контролы. При желании можно всегда выводить простой TextBox. Все упирается только в фантазию аналитика и бюджет проекта.

Шаг 3. Подготовка к пикеру

Для начала вернемся к свойству FieldRenderingControl. В случае, если множественный выбор не разрешен, то в качестве значения этого свойства будет возвращен экземпляр класса CustomUserFieldControl. Случай со множественным выбор я в посте описывать не буду, все это можно посмотреть в исходных кодах на CodePlex и SkyDrive.

Вот частично код класса CustomUserFieldControl:

public class CustomUserFieldControl : UserField
{
    // Пикер
    private CustomUserFieldPicker _picker;
 
    [SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
    protected override void CreateChildControls()
    {
        if (IsFieldValueCached)
        {
            base.CreateChildControls();
        }
        else if (Field != null)
        {
            if (ControlMode != SPControlMode.Display)
            {
                // Иницализация пикера
                _picker = new CustomUserFieldPicker
                          {
                              MultiSelect = ((CustomUserField)Field).AllowMultipleValues
                          };
                Controls.Add(_picker);
                // Задание значения
                SetFieldControlValue(Page.IsPostBack ? Value : ItemFieldValue);
            }
        }
    }
 
    private void SetFieldControlValue(object value)
    {
        //...
    }
 
    public override void Validate()
    {
        //...
    }
}

Простой контрол, унаследованный от Microsoft.SharePoint.WebControls.UserField, который добавляет на страницу пикер для задания значения поля, обеспечивает валидацию и прочее.

Шаг 4. Создание пикера (EntityPicker)

Класс нового поля создан, описание в виде XML-файла готово. Теперь можно переходить к реализации пикера. Сценарий использования нового типа поля будет следующим: выбор пользователей должен быть заменен на выбор сотрудников из соответствующего списка с возможностью поиска сотрудников по их принадлежности к структурному подразделению организации, для чего структура организации должна быть отображена в виде дерева в левой части диалогового окна. Теперь тоже самое в виде картинки:

Объектная модель будет использована описанная мною в постах о Linq to SharePoint.

Пикер в SharePoint 2010 состоит из следующий частей:

  • Сам пикер (EntityPicker). В нем должны быть реализованы методы для валидации введенных данных (метод ValidateEntity). поиска подходящих вариантов в случае ошибки при валидации (метод ResolveErrorBySearch). Также должен быть указан тип для диалога (свойство PickerDialogType);
  • Контрол поиска - строка поиска плюс выпадающий список;
  • Контрол отображения результатов - таблица (по умолчанию) с результатами поиска;

Пикер CustomUserFieldPicker унаследован от класса Microsoft.SharePoint.WebControls.PeopleEditor.

Пикер

Первое, что должно быть реализовано в пикере - валидация введенного значения. Для этого переопределяем метод ValidateEntity:

public override PickerEntity ValidateEntity(PickerEntity entity)
{
    if (entity == null) return null;
    if (entity.IsResolved) return entity;
    var repository = new EmployeeRepository(true);
    var filter = entity.DisplayText;
    var emps = repository.GetEntityCollection(
        x => x.Title.Contains(filter) || x.CellPhone.Contains(filter))
        .Take(2)
        .ToList();
    if (emps.Count == 1)
    {
        entity = emps[0].ToPickerEntity();
    }
    else
    {
        entity.IsResolved = false;
    }
    return entity;
}

Метод принимает объект типа PickerEntity. Если объекта нет, то возвращаем null. Если сущность уже валидна (IsResolved == true), то возвращаем её же. Далее создаем репозиторий для сотрудников и пробуем найти одного из них по введенному тексту (entity.DisplayText). В случае, если заданным критериям отвечает единственный сотрудник, создаем новый объект EntityPicker, используя следующий метод расширитель:

public static PickerEntity ToPickerEntity(this ZhukDataItem value)
{
    return new PickerEntity
                {
                    DisplayText = value.Title,
                    IsResolved = true,
                    EntityType = "ZhukDataItem",
                    Key = value.Id.ToString()
                };
}

В противном случае устанавливаем флаг IsResolved = false. Можно реализовать поиск по должности, тогда можно будет ввести "генеральный", вызвать валидацию и получить валидную сущность, представляющуу генерального директора (если других "генеральных" нет). Если сущность не прошла валидацию, то вызывается метод ResolveErrorBySearch для поиска подходящих вариантов:

protected override PickerEntity[] ResolveErrorBySearch(string unresolvedText)
{
    var repository = new EmployeeRepository(true);
    var emps = repository.GetEntityCollection(
        x => x.Title.Contains(unresolvedText) || x.CellPhone.Contains(unresolvedText))
        .ToList();
    return emps.Select(x => x.ToPickerEntity()).ToArray();
}

В этом методе все предельно просто. Ищем все, что удастся и возвращаем найденное.

Строка поиска

Класс, унаследованный от Microsoft.SharePoint.WebControls.SimpleQueryControl. Сначала избавимся от выпадающего списка возле строки поиска. Для этого воспользуемся свойством ColumnList, которое и является этим списком:

protected override void CreateChildControls()
{
    base.CreateChildControls();
    ColumnList.Visible = false;
}

Со списком покончено. Теперь перед таблицей результатов поиска вставим свой UserControl, содержащий дерево, отражающее орг структуру организации ("/_CONTROLTEMPLATES/EmployeeFinder.ascx").

private EmployeeFinder _finder;
 
protected override void CreateChildControls()
{
    base.CreateChildControls();
    ColumnList.Visible = false;
    // Загружаем контрол
    _finder = Page.LoadControl("/_CONTROLTEMPLATES/EmployeeFinder.ascx") as EmployeeFinder;
    if (_finder != null)
    {
        // Указываем экземпляр диалога
        _finder.Dialog = PickerDialog;
        // Находим контейнер для результатов поиска
        var container = PickerDialog.ResultControl.Parent;
        // Вставляем UserControl в начало
        container.Controls.AddAt(0, _finder);
        // Оборачиваем таблицу результатов в div для разметки страницы
        container.Controls.AddAt(1, new LiteralControl(@"<div id=""DataTableContainer"">"));
        container.Controls.AddAt(3, new LiteralControl(@"</div>"));
    }
}

Теперь перейдем к основному функционалу. Поиск:

protected override int IssueQuery(string search, string group, int pageIndex, int pageSize)
{
    search = (search != null) ? search.Trim() : null;
    if (string.IsNullOrEmpty(search))
    {
        PickerDialog.ErrorMessage = "Нечего искать";
        return 0;
    }
    var repository = new EmployeeRepository(true);
    var employees = repository.GetEntityCollection(
        x => x.Title.Contains(search) || x.CellPhone.Contains(search))
        .ToList();
    if (employees.Count == 0)
    {
        PickerDialog.ErrorMessage = "Ничего не найдено";
        return 0;
    }
    // Создаем таблицу и наполняем её
    var table = CreateDataTable();
    foreach (var employee in employees)
    {
        var row = table.NewRow();
        row["Id"] = employee.Id;
        row["Title"] = employee.Title;
        row["DepartmentTitle"] = employee.DepartmentTitle;
        table.Rows.Add(row);
    }
    PickerDialog.Results = table;
    PickerDialog.ResultControl.PageSize = table.Rows.Count;
    // Указываем параметры столбцов таблицы
    var rc = PickerDialog.ResultControl as TableResultControl;
    if (rc != null)
    {
        rc.ColumnNames.AddRange(new[] { "Id", "Title", "DepartmentTitle" });
        rc.ColumnDisplayNames.AddRange(new[] { "ИД", "ФИО", "Отдел", });
        rc.ColumnWidths.AddRange(new[] { "15%", "50%", "35%" });
    }
    // Возвращаем кол-во найденных элементов
    return table.Rows.Count;
}

И получение сущности из строки:

public override PickerEntity GetEntity(DataRow dataRow)
{
    if (dataRow == null)
        return null;
    var entity = new PickerEntity
    {
        Key = Convert.ToString(dataRow["Id"]),
        DisplayText = Convert.ToString(dataRow["Title"]),
        Description = string.Empty,
        IsResolved = true
    };
    entity.EntityData.Clear();
    entity.EntityData.Add("DepartmentTitle", dataRow["DepartmentTitle"]);
    return entity;
}

Функционал в этих метод прост и, я думаю, дополнительные комментарии излишни.

Результаты поиска

В моем случае я использовал стандартный контрол. Можно реализовать свой, унаследовав его от типа Microsoft.SharePoint.WebControls.PickerResultControlBase.

Виталий Жуков

Виталий Жуков

SharePoint архитектор, разработчик, тренер, Microsoft MVP (Office Development). Более 15 лет опыта работы с SharePoint, Dynamics CRM, Office 365, и другими продуктами и сервисами Microsoft.

Смотрите также

Развертывание списков и библиотек с помощью SPFx-решений

Развертывание списков и библиотек с помощью SPFx-решений

SharePoint. Drag-and-Drop Загрузчик файлов

SharePoint. Drag-and-Drop Загрузчик файлов

CSOM. Загрузка файлов

CSOM. Загрузка файлов

SharePoint List REST API. Часть 2

SharePoint List REST API. Часть 2

SharePoint Framework. Создание веб-части на Angular

SharePoint Framework. Создание веб-части на Angular

SharePoint List REST API. Часть 1

SharePoint List REST API. Часть 1

Презентация с доклада о SharePoint Framework

Презентация с доклада о SharePoint Framework

SharePoint Framework. Создаем AngularJS 1.x Client WebPart

SharePoint Framework. Создаем AngularJS 1.x Client WebPart

SharePoint. Регистрация CSS и JavaScript с помощью DelegateControl

SharePoint. Регистрация CSS и JavaScript с помощью DelegateControl

SharePoint. Расширяем REST API

SharePoint. Расширяем REST API

SharePoint Excel Services. Создаем кредитный калькулятор

SharePoint Excel Services. Создаем кредитный калькулятор

SharePoint Ribbon API. Использование ToggleButton

SharePoint Ribbon API. Использование ToggleButton

SharePoint 2013. How To: настройка входящей почты для разработчиков

SharePoint 2013. How To: настройка входящей почты для разработчиков

Мифы и правда о Linq to SharePoint

Мифы и правда о Linq to SharePoint

5 особенностей SPSiteDataQuery

5 особенностей SPSiteDataQuery

SharePoint 2013. Введение в SharePoint App. Часть 2

SharePoint 2013. Введение в SharePoint App. Часть 2

SharePoint 2013. Введение в SharePoint App. Часть 1

SharePoint 2013. Введение в SharePoint App. Часть 1

Превью для веб-части в SharePoint 2010/2013

Превью для веб-части в SharePoint 2010/2013

SharePoint 2013. Еще немного о новых контролах

SharePoint 2013. Еще немного о новых контролах

SharePoint 2013. Контрол ClientPeoplePicker

SharePoint 2013. Контрол ClientPeoplePicker

SharePoint 2013. Контрол ImageCrop

SharePoint 2013. Контрол ImageCrop

SharePoint 2013. Тип поля Geolocation

SharePoint 2013. Тип поля Geolocation

SharePoint 2010. Длительные операции с обновляемым статусом

SharePoint 2010. Длительные операции с обновляемым статусом

Linq to SharePoint. Создаем ContentIterator

Linq to SharePoint. Создаем ContentIterator

Linq to SharePoint. Получение данных из другой коллекции сайтов

Linq to SharePoint. Получение данных из другой коллекции сайтов

Linq to SharePoint. Версионность

Linq to SharePoint. Версионность

SharePoint. Получение URL-адреса иконки для документа

SharePoint. Получение URL-адреса иконки для документа

SharePoint 2010. PostBack для Fluent Ribbon API

SharePoint 2010. PostBack для Fluent Ribbon API

Linq to SharePoint. Блокировка документов

Linq to SharePoint. Блокировка документов

Linq to SharePoint. Паттерн Repository

Linq to SharePoint. Паттерн Repository

Linq to SharePoint. Получение мета-данных списка

Linq to SharePoint. Получение мета-данных списка

Linq to SharePoint. Мапинг полей

Linq to SharePoint. Мапинг полей

Linq to SharePoint. Формирование данных для ProcessBatchData

Linq to SharePoint. Формирование данных для ProcessBatchData

Linq to SharePoint. Сравнение производительности с Camlex.NET

Linq to SharePoint. Сравнение производительности с Camlex.NET

Linq to SharePoint. Часть 5. Поля Choice и MultiChoice

Linq to SharePoint. Часть 5. Поля Choice и MultiChoice

Linq to SharePoint. Часть 4. Dynamic LINQ

Linq to SharePoint. Часть 4. Dynamic LINQ

Linq to SharePoint. Особенности. Часть 3

Linq to SharePoint. Особенности. Часть 3

Linq to SharePoint. Особенности. Часть 2

Linq to SharePoint. Особенности. Часть 2

SharePoint 2010. PeopleEditor. Установка значения

SharePoint 2010. PeopleEditor. Установка значения

SharePoint 2010. Настройка входящей почты для кастомного списка

SharePoint 2010. Настройка входящей почты для кастомного списка

Linq to Sharepoint. Особенности

Linq to Sharepoint. Особенности

EntityFramework. Оптимистические блокировки

EntityFramework. Оптимистические блокировки