Создание нового типа поля для MS SharePoint на примере простого проверяемого поля
При работе с SharePoint часто возникает необходимость сделать свое собственное поле для каких-либо специфических задач. Одна из таких задач — возможность проверки текстовых полей, например на правильность заполнения Email или каких-либо данных об Организации: ИНН, КПП и д.р.
Самым простым и удобным в этом случае является применение регулярных выражений, поэтому на них и остановимся.
Начнем с внутреннего представления поля в Sharepoint. Это класс отнаследованный от класса SPField. Сами поля хранятся в базе Sharepoint в виде зашифрованного XML. На основании ХML(схемы поля) создается объект отнаследованный от SPField. Схема обычного текстового поля выглядит так:
<Field ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="Title" Group="_Hidden" Type="Text" DisplayName="Наименование" Required="TRUE" FromBaseType="TRUE" />
Для начала создадим класс нашего нового поля:
class RegExpField : SPFieldText
{
public RegExpField(SPFieldCollection fields, string fieldName) : base(fields, fieldName)
{
}
public RegExpField(SPFieldCollection fields, string typeName, string displayName)
: base(fields, typeName, displayName)
{
}
}
У поля должно быть 2 настраиваемых свойства — это свойство хранящие регулярное выражения для его валидации и свойство, хранящие текст ошибки выводящиеся в случаи несоответствии строки регулярному выражению.
С кастомными свойства у SharePoint полей есть определенные сложности, самым оптимальным является использования да простят меня Боги рефлексии. Нам нужно добавить 2 метода в наше поле:
//Записываем значение поля в текущую схему через рефлексивный вызов метода
private void SetFieldAttribute(string attribute, string value)
{
Type baseType;
BindingFlags flags;
MethodInfo mi;
baseType = typeof(RegExpField);
flags = BindingFlags.Instance | BindingFlags.NonPublic;
mi = baseType.GetMethod("SetFieldAttributeValue", flags);
mi.Invoke(this, new object[] { attribute, value });
}
//Считываем значение поля из текущей схемы через рефлексивный вызов метода
private string GetFieldAttribute(string attribute)
{
Type baseType;
BindingFlags flags;
MethodInfo mi;
baseType = typeof(RegExpField);
flags = BindingFlags.Instance | BindingFlags.NonPublic;
mi = baseType.GetMethod("GetFieldAttributeValue",
flags,
null,
new Type[] { typeof(String) },
null);
object obj = mi.Invoke(this, new object[] { attribute });
if (obj == null)
return "";
else
return obj.ToString();
}
Добавим 2 свойства в поле:
public string ValidRegExp
{
get
{
return GetFieldAttribute("ValidRegExp");
}
set
{
SetFieldAttribute("ValidRegExp", value);
}
}
public string ErrorMessage
{
get
{
return GetFieldAttribute("ErrorMessage");
}
set
{
SetFieldAttribute("ErrorMessage", value);
}
}
Теперь нужно добавить возможность редактировать настройки поля пользователю. Для этого создаем UserControl RegExpFieldEdit.ascx и наследуем его от интерфейса IFieldEditor. У интерфейса одно свойство и два метода которые нужно переопределить. Выглядеть это будет так:
public bool DisplayAsNewSection
{
get
{
return true;
}
}
//Устанавливаем значения поля в контролы
public void InitializeWithField(SPField field)
{
if (!IsPostBack)
{
if (field is RegExpField)
{
var Validfield = field as RegExpField;
RegExp.Text = Validfield.ValidRegExp;
ErrorMessage.Text = Validfield.ErrorMessage;
}
}
}
//Переносим значения контролов в поле
public void OnSaveChange(SPField field, bool isNewField)
{
if (field is RegExpField)
{
var Validfield = field as RegExpField;
Validfield.ValidRegExp = RegExp.Text;
Validfield.ErrorMessage = ErrorMessage.Text;
}
}
Следующим шагом нужно сказать SharePoint, что все, что мы создали — является полем. Для этого создадим XML в которым пропишем новый тип поля. Для начала «замапим» папку XML из SharePoint в наш проект и создадим новый XML файл c именем Fldtypes_RegExpField.xml, и содержимым:
<FieldTypes>
<FieldType>
<Field Name="TypeName">RegExpField</Field>
<Field Name="ParentType">Text</Field>
<Field Name="TypeDisplayName">Проверяемое текстовое поля</Field>
<Field Name="TypeShortDescription">Проверяемое текстовое поля</Field>
<Field Name="FieldTypeClass">RegExpField.RegExpField, $SharePoint.Project.AssemblyFullName$</Field>
<Field Name="FieldEditorUserControl">/_controltemplates/15/RegExpFieldEdit.ascx</Field>
</FieldType>
</FieldTypes>
Теперь нужно разобраться с методами рендеринга поля. В SharePoint есть два метода отрисовки поля:
- Серверный рендеринг – для этого используется некий WebControl отнаследованый от BaseFieldControl, данная методика является устаревший в рамках SharePoint 2013. Поэтому мы не будет создавать этот контрол.
- Клиентский рендеринг (Client side rendering) – под ним понимается отриcовка поля непосредственно использующая JavaScript в браузере клиента. Он появился в SharePoint 2013 и повсеместно там используется.
Для нашего поля будем использовать клиентский рендеринг. Для начала создадим некоторый JS файл RegExpField.js в котором будет производиться отрисовка поля на клиенте. Для привязки файла к нашему полю используется механизм JSLink. У каждого поля есть свойство JSLink. В нем указываются JS файлы которые необходимы загружать на клиент для работы отрисовки поля. Перегрузим свойство в нашем классе поля.
public override string JSLink
{
get
{
return "/_layouts/15/RegExpField/RegExpField.js";
}
}
Еще нужно передавать наши новые параметры на клиент. Для этого есть специальный метод в SPField. Перегрузим и добавим в него наши параметры. Вот как это выглядит:
public override Dictionary<string, object> GetJsonClientFormFieldSchema(SPControlMode mode)
{
var formtctx = base.GetJsonClientFormFieldSchema(mode);
formtctx["ValidRegExp"] = ValidRegExp;
formtctx["ErrorMessage"] = ErrorMessage;
return formtctx;
}
Здесь мы берем уже сформированный контекст и добавляем в него наши свойства. Данные из этого метода сериализуются SharePoint'ом в JSON при помощи класса JavaScriptSerializer.
Теперь нужно зарегистрировать шаблон отрисвоки поля на клиенте для этого напишем в JS файле следующий код:
RegExpFieldTemplate = function RegExpFieldTemplate () {
}
RegExpFieldTemplate.$$cctor = function RegExpFieldTemplate $$$cctor() {
if (typeof (SPClientTemplates) != "undefined")
SPClientTemplates.TemplateManager.RegisterTemplateOverrides(RegExpFieldTemplate.createRenderContextOverride()); // регистрируется объект с функциями отрисовки
}
RegExpFieldTemplate.createRenderContextOverride = function () {
var RegExpFieldTemplateContext = {};
RegExpFieldTemplateContext.Templates = {};
RegExpFieldTemplateContext.Templates['Fields'] = {
RegExpField: {
View: RegExpFieldTemplate.renderViewControl,
DisplayForm: RegExpFieldTemplate.renderDisplayControl,
NewForm: RegExpFieldTemplate.renderEditControl,
EditForm: RegExpFieldTemplate.renderEditControl,
}//создаем объект в котором есть поле с именем нашего типа поля, в этом объекте определены функции отрисовки для каждого возможного варианта
};
return RegExpFieldTemplateContext;
}
function RegExpField_init() {
RegExpFieldTemplate.$$cctor();
};
RegExpField_init();
Теперь разберем отрисовку каждого варианта отображения в отдельности.
Начнем c отрисовки на отображении(View)
RegExpFieldTemplate.renderViewControl = function (renderCtx, field, item, list) {
if (renderCtx.inGridMode === true) {
field.AllowGridEditing = false; // Отключаем режим редактирования поля если выбран режим GridView
}
return STSHtmlEncode(item[field.Name]);//Берем значения поля из item. Предварительно производим Encode Html символов и отправляем значение в рендеринг
}
В качестве возвращаемого значения функции возвращается html разметка в виде строки.
На форме просмотра будет использовать следующий код:
RegExpFieldTemplate.renderDisplayControl = function (renderCtx) {
return STSHtmlEncode(renderCtx.CurrentFieldValue);//Берем значение из контекста сформированного для поля и производим Encode Html
}
Остался рендеринг поля на форме редактирования и на форме создания элемента. Вот что получилось:
//Создаем объект валидатора поля
RegExpFieldTemplate.ValidatorValue = function (stringRegExp, errorMessage) {
//в объекте должна быть всего 1 функция она производит валидацию
RegExpFieldTemplate.ValidatorValue.prototype.Validate = function (value) {
//Проверяем наличие данных в поле и наличие регулярного выражения
if (value && stringRegExp) {
var reg = new RegExp(stringRegExp);
//Проверяем проходит ли валидацию значение поля
if (!reg.test(value)) {
//В качестве возвращаемого значения создается объект,
// в котором первый параметр показывает наличие ошибки, второй текст ошибки
return new SPClientFormsInclude.ClientValidation.ValidationResult(true, errorMessage);//
}
}
return new SPClientForms.ClientValidation.ValidationResult(false);
};
}
RegExpFieldTemplate.renderEditControl = function (rCtx) {
if (rCtx == null)
return '';
var frmData = SPClientTemplates.Utility.GetFormContextForCurrentField(rCtx);// Получение данных формы, функция по контексту
//возвращает специальный объект с данными и функциями для регистрации событий поля
if (frmData == null || frmData.fieldSchema == null)
return '';
var _inputElt;
var _value = frmData.fieldValue != null ? frmData.fieldValue : '';// В случаи если с сервера придут не корректные данные
var _inputId = frmData.fieldName + '_' + '_$RegExp' + rCtx.FormUniqueId;//Формируется Id input'а ввода
var validators = new Eos.Fields.ClientControls.ClientValidation.ValidatorSet();//Создается объект валидатора, в него нужно будет записать все используемые в поле валидаторы
if (frmData.fieldSchema.Required) {//Проверка на наличие включеной настройки обязательности заполнения данного поля
// Если поле является обязательным к заполнению нужно добавить специальный валидатор проверки заполненности поля
validators.RegisterValidator(new Eos.Fields.ClientControls.ClientValidation.RequiredValidator());
}
//Здесь происходит регистрация нашего валидатора указанного в настройках поля
validators.RegisterValidator(new RegExpFieldTemplate.ValidatorValue(rCtx.CurrentFieldSchema.ValidRegExp,rCtx.CurrentFieldSchema.ErrorMessage));
//Регистрация объекта валидации
frmData.registerClientValidator(frmData.fieldName, validators);
//регистрируется функция вызываемая после добавления HTML разметки в DOM
frmData.registerInitCallback(frmData.fieldName, InitControl);
//регистрируется функция вызываемая при необходимости фокусировки на данном поле, допустим
//после того как поле не прошло валидацию
frmData.registerFocusCallback(frmData.fieldName, function () {
if (_inputElt != null) {
_inputElt.focus();
if (browseris.ie8standard) {
var range = _inputElt.createTextRange();
range.collapse(true);
range.moveStart('character', 0);
range.moveEnd('character', 0);
range.select();
}
}
});
//регистрируется функция вызываемая для вывода ошибки поля
frmData.registerValidationErrorCallback(frmData.fieldName, function (errorResult) {
//Стандартная функция рисующая ошибку у заданного элемента, рисуется в виде span'a внизу поля
SPFormControl_AppendValidationErrorMessage(_inputId, errorResult);
});
//регистрируется функция вызываемая для получения значений из поля
frmData.registerGetValueCallback(frmData.fieldName, function () {
return _inputElt == null ? '' : _inputElt.value;
});
//обновляет значение поля хранящиеся в скрытом hidden (На самом деле так и не понял зачем это делается, но решил добавить)
frmData.updateControlValue(frmData.fieldName, _value);
//Формируем разметку поля на основании контекста
var result = '<span dir="' + STSHtmlEncode(frmData.fieldSchema.Direction) + '">';
result += '<input type="text" value="' + STSHtmlEncode(_value) + '" maxlength="' + STSHtmlEncode(frmData.fieldSchema.MaxLength) + '" ';
result += 'id="' + STSHtmlEncode(_inputId) + '" title="' + STSHtmlEncode(frmData.fieldSchema.Title);
result += '" class="ms-long ms-spellcheck-true ' + (rCtx.CurrentFieldSchema.DoubleWidth ? 'InputDoubleWidth' : '') + ' " />';
result += '<br /></span>';//
return result;
//Описываем функцию которая срабатывает после добавления разметки в DOM
function InitControl() {
//Получаем наш Input
_inputElt = document.getElementById(_inputId);
if (_inputElt != null)
//Добавляем событие изменения
AddEvtHandler(_inputElt, "onchange", OnValueChanged);
}
//Описываем функцию изменения в input
function OnValueChanged() {
if (_inputElt != null)
//обновляет значение поля хранящиеся в скрытом hidden (На самом деле так и не понял зачем это делается, но решил добавить)
frmData.updateControlValue(frmData.fieldName, _inputElt.value);
}
}
Тут я постарался максимально описать все в комментариях к коду, поэтому думаю дополнительно описывать тут нечего.
В общем и целом, наше поле готово. Осталось развернуть решение на SharePoint и настроить наше новое поле. Поле в Sharepoint будет выглядеть так:
Создание нового типа поля такого уровня занимает пару часов. И оно может сильно облегчить работу с формами пользователей. Его так же можно легко расширить добавив в него маску ввода, допустим с помошью библиотеки jQuery Masked Input
Все исходники доступны на GitHub.