Создание нового типа поля для MS SharePoint на примере простого проверяемого поля

При работе с SharePoint часто возникает необходимость сделать свое собственное поле для каких-либо специфических задач. Одна из таких задач — возможность проверки текстовых полей, например на правильность заполнения Email или каких-либо данных об Организации: ИНН, КПП и д.р.

3950c8c9e85a427a8664c9703d67c6e6.png
Самым простым и удобным в этом случае является применение регулярных выражений, поэтому на них и остановимся.
Начнем с внутреннего представления поля в 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 будет выглядеть так:

d85bee10046c4a55b3c1293b5b16ca47.png

Создание нового типа поля такого уровня занимает пару часов. И оно может сильно облегчить работу с формами пользователей. Его так же можно легко расширить добавив в него маску ввода, допустим с помошью библиотеки jQuery Masked Input

Все исходники доступны на GitHub.

© Habrahabr.ru