Создание типа поля в 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.