При работе с 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.

Комментарии (11)


  1. ZOXEXIVO
    22.10.2015 22:22
    +6

    Испытал радость, что когда-то я не выбрал путь Sharepoint.


    1. justmara
      23.10.2015 01:36
      +4

      эта радость ничто по сравнению с воспоминаниями тех, кто пощупал, но соскочил.


  1. gandjustas
    24.10.2015 15:57

    Зачем было создавать новый тип поля, если тоже самое можно сделать, просто поменяв шаблоны для вывода? Для серверной валидации можно event receiver сделать. Кода будет в разы меньше, деплоить проще, результат лучше.


    1. BOBS13
      24.10.2015 16:26

      Ну как минимум для шаблонов ввода есть проблема, если его применить, программное создание элемента падает. А ресивер не подошел потому что, хотелось что пользователь сразу видел, что произошло сразу берез передачи на сервер, и на самом деле уже в продакшине у этого поле используется маска ввода.


      1. gandjustas
        26.10.2015 10:27

        Вы про какие шаблоны? Я говорю про шаблоны для CSR. Каким образом что-то падает? Ресивер дополняет клиентскую валидацию.
        Зачем вам сusom field type? Я очень сомневаюсь что пользователи ходят и создают новые поля. А для частного решения можно и менее тяжелый способ применить.


        1. BOBS13
          26.10.2015 10:42

          Во-первых полей около десятка. Тыкают не пользователи, а внедренцы — это совсем другие люди. А внедренцу намного проще создать новое поле, и задать ему шаблон. чем всякой другой ересью заниматься, тем более еще ресивер вешать. Это не маленькое решение для одиночной задачи, а тиражное решение. Тут уже совсем другие приоритеты.


          1. gandjustas
            26.10.2015 11:14

            Для тиражируемого решения кастомные типы полей — слишком проблемная штука. Не поддерживаются в REST, плохо работают в CSOM, поиске и workflow (вам повезло, у вас просто текстовое поле — проблем меньше). Обновление типов полей требует рестарта IIS и, самое главное, это не работает в Office 365.

            Я бы сделал по-другому:
            1) Обычные текстовые поля
            2) Дополнительная ссылка в настройках поля для валидации
            3) На странице настраивается regexp для поля и сохраняет данные в свойствах (property bag) и в файл, который подключается в JSLink
            4) JS с валидатором, подключается через Custom Action, а скрипт сохраненный в 3 дергает класс валидатора для CSR
            5) Эвентресивер cо scope=«Site» (срабатывает на всех списках) проверяет метаданные поля и выполняет нужную валидацию.
            Кода на выходе будет примерно столько же, но не будет всех проблем кастом-типов-полей.


            1. BOBS13
              26.10.2015 12:15

              1) Поддержка SharePoint Online не нужна.
              2) Новых типов полей в нашем решении уже больше 10-ка, каких-там только нет. Особо больших проблем в поиске не было. Тем более решение должно работать в режиме реального времени и использование систем поиска не очень удобно.
              3) Вы предлагаете при каждом сохранении элементов проверять их метаданные, зачем, если это нужно в основном только в справочных списках. Это лишняя нагрузка и она не к чему.
              Мое мнение вариант с типом поля намного проще и логичнее.


              1. gandjustas
                26.10.2015 12:32

                1) Поддержка SharePoint Online не нужна.

                А вы готовы поставить $10000, что поддержка облака не понадобится в течение 3 лет?

                2) Новых типов полей в нашем решении уже больше 10-ка, каких-там только нет. Особо больших проблем в поиске не было.
                Вы просто не знаете о них ;) И вам очень везет если заказчик не знает.

                Тем более решение должно работать в режиме реального времени и использование систем поиска не очень удобно.
                ИНН и реальное время? Вы о чем? Я же прекрасно знаю что за системы вы делаете, реальное время там не нужно от слова вообще.

                3) Вы предлагаете при каждом сохранении элементов проверять их метаданные, зачем, если это нужно в основном только в справочных списках. Это лишняя нагрузка и она не к чему.
                При желании можно только на выбранных списках включать, если вас это так беспокоит. Эвент-ресивер нужен только для защиты от хакеров, которые воспользуются API для заведения значений. Для пользователей все что нужно — валидатор на клиенте. Можно даже шаблон рендеринга не копипастить, а прицепить валидатор к стандартному шаблону.

                Мое мнение вариант с типом поля намного проще и логичнее.

                Это только ваше мнение. Тем не менее типы полей несут огромное количество недостатков.


                1. BOBS13
                  26.10.2015 13:25

                  Пока sharepoint online, предлагает, что он предлагает. Использовать его все равно не получится, слишком малый уровень возможности кастомизации.
                  Режим реального времени это у задача другая, и не вохможность использования в системе стандартных методов поиска отсюда и вытекает. Все недостатки понятны, я пытаюсь только сказать, что для конкретной ситуации этот выход вполне предпочтителен. Тем более в статье, основной упор делается на то как сделать новый тип поля.


                  1. gandjustas
                    26.10.2015 13:59

                    Пока sharepoint online, предлагает, что он предлагает. Использовать его все равно не получится, слишком малый уровень возможности кастомизации.

                    99% того, что можно сделать в наземном шарике можно сделать и в облачном. Только другими средствами. Я вам и предлагаю использовать другие средства.

                    ЗЫ. Вы НЕ попадаете в оставшийся 1% со своими задачами.