Создание Сustom Field Types

Поиск по сайту
    

Создание Сustom Field Types

Содержание

Введение
Строение Custom Field Type
Базовый тип поля BaseTextField
Наследование BaseTextField
Создание Сustom Field Class и Сustom Field Control
Перегрузка RenderFieldForDisplay() и RenderFieldForInput()
Подключение ASCX-шаблона
Создание собственных свойств Custom Field Type
Валидация в Custom Field Type
Способы валидации
Добавление валидаторов ASP.NET
Перегрузка метода GetValidatedString()
Создание Custom Field Value.
Способы реализации Custom Field Value
Сериализация в промежуточном классе
Базовый тип поля SPFieldMultiColumn
Наследование SPFieldMultiColumn

 

Введение

Напиши с начала, что такое Custom Field Type, где и для чего используется.  Вкратце.
С помощью создания Custom Field Type можно расширить стандартный набор полей, доступных в Windows SharePoint Services (WSS) 3.0 и Office SharePoint Server (MOSS) 2007. Задача создания Custom Field Type считается сложной, частично в связи с отсутствием исчерпывающей документации.
В статье рассматриваются базовые аспекты создания Custom Field Type, улучшение пользовательского интерфейса поля с помощью валидации, а также модификацию формата, в котором хранятся данные поля.

 

СтроениеCustom Field Type

Базовые типы полей, поставляемые с SharePoint, состоят из следующих компонентов.
Field type definition является XML-файлом в файловой системе. Находится в директории [..]\12\TEMPLATE\XML. Имя файла формируется по принципу fieldtype_[yourname].xml. SharePoint автоматически считывает все определения полей из XML-файлов, имена которых начинаются с fieldtype_.
Field Type Class – это класс поля, который обеспечивает централизованный доступ к другим его компонентам. Классы Field Type Class стандартных полей SharePoint знакомы большинству разработчиков, например: SPField, SPFieldText, SPFieldLookup и др. Основное использование классов стандартных полей, это наследование.
Field Control Class – это класс сходный по функциям с  ASP.NET Server Control. Основное предназначение, которого отрисовка поля в формах создания и редактирования SharePoint. Базовый класс для наследования здесь BaseTextField, и др.
Field Rendering Template – это декларативный шаблон поля. Он представляет собой ASCX-файл, отличающийся от ASP.NET User Control тем, что он не содержит ссылки на CodeBehind-класс. Вместо этого, он помещается в файловую систему и Field Control Class подключает его с помощью метода Page.LoadControl().
Value Class класс, управляющий значением поля, рисунок 1. Основное назначение данного класса, управление процессом конвертирования значения поля в строку для сохранения в БД. А также обратным процессом десериализации сохраненных данных.
img
Рис.1 – Управление значением поля
При создании собственного типа поля, не обязательна реализация всех компонентов. Возможно использование существующих базовых компонентов.

Базовый тип поля BaseTextField

Класс BaseTextFieldявляется базовой имплементацией текстового однострокового поля. Знакомое многим разработчикам поле SPFieldText унаследовано от BaseTextField. BaseTextField отличается от SPFieldText следующим:

Тип поля BaseTextField удобно использовать для наследования, когда целью является хранение неформатированного текста общей длиной не более 255 символов.
В других случаях можно обратиться к полю SPFieldTextMultiLine, поддерживающему хранение форматированного текста неограниченной длины.

Наследование BaseTextField

В ходе работы был использован инструмент WSPBuilder (http://codeplex.com/wspbuilder).

2.1.1 Создание Сustom Field Class и Сustom Field Control

Начнем создание с нового проекта WSPBuilder. Создадим XML-файл fldtypes_MyField.xml представляющий собой определение CustomFieldDefinition, рисунок 2.
img
Рис. 2 –Создание XWL-файла
Заполним его содержимым (для справки см. комментарии в тексте файла).
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
<FieldType>
<!--
Имя поля используемое при создании экземпляров в CAML -->
<Field Name="TypeName">MyField</Field>
<!--
Имя родительского поля (из стандартных/custom-полей) -->
<Field Name="ParentType">Text</Field>
<!--
Имя отображаемое в списке столбцов SharePoint -->
<Field Name="TypeDisplayName">My Field</Field>
<!--
Имя отображаемое при создании нового столбца (radio button list) -->
<Field Name="TypeShortDescription">My custom field type</Field>
<!--
Может ли быть создано пользователем? -->
<Field Name="UserCreatable">TRUE</Field>
<!--
Five-part class name
(класс - 1 часть, сборка - 4 части)
(сборка берется из любой feature.xml) -->
<Field Name="FieldTypeClass">
WSPBuilderProject2.MyField,
WSPBuilderProject2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=bbc60ea8d5ab3107
</Field>

</FieldType>
</FieldTypes>
Получить значение five-part class name можно добавив в проект Feature with Receiver. И скопировать assembly и class из feature.xml. При этом ссылка на сборку Microsoft.SharePoint добавится автоматически. 
Создадим класс MyField унаследованный от SPFieldText. Five-partclassname, должно указывать именно на MyField.
public class MyField : SPFieldText
{
}
 Имплементируем необходимые конструкторы для правильного наследования:
public MyField(SPFieldCollection fields, string fieldName)
: base(fields, fieldName)
{

}

public MyField(SPFieldCollection fields, string typeName,
string displayName)
: base(fields, typeName, displayName)
{

}
Добавим класс MyFieldControl унаследованный от класса BaseTextField:
public class MyFieldControl : BaseTextField
{
}
BaseTextField является "дальним родственником" ASP.NET ServerControl (т.к. между ними цепочка наследования из классов поставляемых с SharePoint). Также как ServerControl, он проходит через Page Life Cycle, и поддерживает методы CreateChildControls(), OnLoad(), RenderContents() и др.
Привяжем класс MyFieldControl к классу MyField через свойство FieldRenderingControl. При этом необходимо присвоить свойству FieldName значение поля StaticName класса MyField.
public override BaseFieldControl FieldRenderingControl
{
get
{
MyFieldControl ctrl = new MyFieldControl();
ctrl.FieldName = this.InternalName;
return ctrl;
}
}
Таким образом, были созданы два основных класса Custom Field Class (MyField) и Custom Field Control (MyFieldControl). Далее изменим внешний вид поля, в режиме редактирования и создания нового элемента. Для этого перегрузим RenderFieldForDisplay() и RenderFieldForInput() и метод RenderFieldForDisplay() у серверного контрола.
/// <summary>
/// Отрисовка поля в View-режиме
/// </summary>
protected override void RenderFieldForDisplay(HtmlTextWriter output)
{
output.Write("<h5>Рамка отрисована из RenderFieldForDisplay</h5>");
output.Write("<div style=\"border: black 1px dotted;\">");

base.RenderFieldForDisplay(output);

output.Write("</div>");
}
Для выполнения базовой процедуры отрисовки можно воспользоваться методом RenderFieldForDispay базового класса.
Перегрузка методаRenderFieldForDisplay() является не обязательной. Достаточно воспользоваться RenderPatterns, что к тому же является более универсальным решением. Информация о  RenderPatterns можно найти на странице: http://msdn.microsoft.com/en-us/library/aa544291.aspx.
Затем, перегрузим метод RenderFieldForInput()
/// <summary>
/// Отрисовка поля в Edit-режиме
/// </summary>
protected override void RenderFieldForInput(HtmlTextWriter output)
{
output.Write("<h5>Рамка отрисована из RenderFieldForInput</h5>");
output.Write("<div style=\"border: black 1px dotted; padding: 10px;\">");

            base.RenderFieldForInput(output);

            output.Write("</div>");
}
В методах RenderFieldForDisplay() и RenderFieldForInput() следует производить только простые модификации внешнего вида, базового типа поля. Для сложных модификаций, для отображения нескольких элементов управления, и др. сложных задач следует использоватьASCX-шаблон.
Кроме того, в случае использования ASCX-шаблона, его изготовление может быть выполнено другим специалистом, например веб-дизайнером знакомым c ASP.NET.

2.1.2 Подключение ASCX-шаблона

Создадим файл ASCX-шаблона MyField.ascx. Используемый при создании Custom Field шаблон отличается от ASP.NETUserControl тем, что при его разметке используется тэг <SharePoint:RenderingTemplate>. Содержимое этого тэга должно быть заключено внутри дочернего тега <Template>. 
В типе проекта WSPBuilder существуют трудности прямым созданием ASCX-контролла. При попытке выполнить Add Item для проекта, в диалоговом окне отсутствует ASCX добавления нового элемента. Решение этой проблемы заключается в  добавлении к проекту TextFile, с последующим переименованием и добавлением директивы @Control, @Assembly, рисунок 3.
img
Рис.3 – Добавление директив
Созданный описанным выше способом файл MyField.ascx, заполним следующим содержимым. Значение атрибута ID у элемента SharePoint:RenderingTemplate должно быть уникальным. Поскольку SharePoint считывает все файлы, находящиеся в директории CONTROLTEMPLATE, затем ищет среди шаблонов файл, с указанным ID.
<%@ Control Language="C#" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint"
Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
Namespace="Microsoft.SharePoint.WebControls" %>

<SharePoint:RenderingTemplate ID="MyField" runat="server">
<Template>
Рамка TextBox отрисована в ASCX
<asp:TextBox ID="TextField" MaxLength="255" runat="server" BackColor="Pink"
Font-Bold="true" BorderStyle="Dotted" BorderColor="DarkBlue" />
</Template>
</SharePoint:RenderingTemplate>
Подключим ASCX-шаблон к Custom Field Class с помощью перегрузки свойства DefaultTemplateName.
protected override string DefaultTemplateName
{
get
{
return "MyField"; // ID шаблона в ASCX-файле
}
}
Контрол поля (наследник BaseTextField) является так же и наследником TemplateControl, который требует лишь задания свойства DefaultTemplateName для правильного подключения шаблона.
Напишем код метода CreateChildControls(). В коде внутри CustomFieldControl можно использовать метод FindControl() для получения ссылок, на элементы управления, объявленные в декларативном шаблоне. Для обращения к родителю-шаблону используется переменная-член TemplateContainer (см. ниже).
Label lblInfo;

        protected override void CreateChildControls()
{
base.CreateChildControls();

            TextBox tb = TemplateContainer.FindControl("TextField") as TextBox;
if (tb != null) {
tb.TextChanged += new EventHandler(tb_TextChanged);
}

            // Add label
lblInfo = new Label() { ID = "lblInfo" };
Controls.Add(lblInfo);
}
Обработаем событие TextChanged для примера обработки событий элементов управления внутри Custom Field Control.
void tb_TextChanged(object sender, EventArgs e)
{
TextBox tb = (TextBox)sender;
lblInfo.Text = "New text: " + tb.Text;
}
После всех проделанных действия было создано поле MyField, которое содержит измененные формы создания и редактирования элементов. Изменения произведены с помощью кода и с помощью декларативной разметки. Также приведен пример создания интерактивного интерфейса. Специальный класс FieldValueне был реализован, поэтому поле хранит данные таким же образом, как и поля базового типа (BaseTextField).

Создание собственных свойств Custom Field Type

Создадим новый проект WSPBuilder под названием RegExField. Затем, добавим новый Custom Field Type одним из способов:

Добавим свойства в XML-определение поля (TEMPLATE\XML\fldtypes_RegExField.xml).
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
<FieldType>
<!--
Имя поля используемое при создании экземпляров в CAML'е -->
<Field Name="TypeName">RegExField</Field>
<!--
Имя родительского поля (из стандартных/custom-полей) -->
<Field Name="ParentType">Text</Field>
<!--
Имя отображаемое в списке столбцов SharePoint -->
<Field Name="TypeDisplayName">Regular Expression Text</Field>
<!--
Имя отображаемое при создании нового столбца (radio button list) -->
<Field Name="TypeShortDescription">Text with regular expression validation</Field>
<!--
Может ли быть создано пользователем? -->
<Field Name="UserCreatable">TRUE</Field>
<!--
Five-part class name
(класс - 1 часть, сборка - 4 части)
(сборка берется из любой feature.xml) -->
<Field Name="FieldTypeClass">
RegExField.RegExField,
RegExField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=15fb88dcbf0850c3
</Field>

<!--
    Объявляем свойства поля-->
    <PropertySchema>
      <Fields>
        <Field Name="ErrorMessage"
        DisplayName="Error message text"
        Type="Text">
        </Field>
        <Field Name="ValidationExpression"
        DisplayName="Regular expression to validate input against"
        Type="Text">
        </Field>
      </Fields>
      <Fields></Fields>
    </PropertySchema>

  </FieldType>
</FieldTypes>
Добавим в field-класс свойство CustomPropertyNamesи внесем в него список добавленных выше свойств.
private static string[] CustomPropertyNames =
new string[] {
"ErrorMessage",
"ValidationExpression"
};
Добавим свойства к Custom Field Class используя методы GetCustomProperty(), SetCustomProperty().
                     private const string ErrMsg_ValidationExpression = "Задано неверное регулярное выражение для поля RegExField";

        public string ErrorMessage
{
// code from WSPBuilder Custom Field Template
get { return this.GetCustomProperty("ErrorMessage") + ""; }
set { this.SetCustomProperty("ErrorMessage", value); }
}
public string ValidationExpression
{
// code from WSPBuilder Custom Field Template
get { return this.GetCustomProperty("ValidationExpression") + ""; }
set
{
try
{
new Regex(value, RegexOptions.IgnoreCase);
}
catch (Exception ex)
{
throw new ArgumentException(ErrMsg_ValidationExpression,
"ValidationExpression", ex);
}
this.SetCustomProperty("ValidationExpression", value);
}
}
Скопируем код заключенный в region под названием «Property storage and bug workarounds» из шаблона Custom Field Type который используется в WSPBuilder.
Также необходимо включить вызовы добавленных методов в оба конструктора CustomFieldClass и в метод Update().
public RegExField(SPFieldCollection fields, string fieldName)
: base(fields, fieldName)
{
InitProperties(); // call WSPBuilder Template method
}

        public RegExField(SPFieldCollection fields, string typeName, string displayName)
: base(fields, typeName, displayName)
{
InitProperties(); // call WSPBuilder Template method
}

        #region Code from WSPBuilder Custom Field Template

        // WSPBuilder Template code
#region Property storage and bug workarounds - do not edit

#endregion

        public override void Update()
{
SaveProperties();// call WSPBuilder Template method
base.Update();
}

 

        #endregion

Валидация в Custom Field Type

Способы валидации

4.1 Добавление валидаторов ASP.NET

Прежде чем добавлять валидаторы, следует иметь ввиду EmptyStringValidator, который автоматически встраивается в форму редактирования, если поле помечено как обязательное. Перегрузим метод CreateChildControls(). Отменим добавление валидатора, если валидация не требуется или не может быть произведена (в случаях если поле не привязано, и в случаях когда мы находимся в режимах Display или Invalid).
protected override void CreateChildControls()
{
if (this.Field == null
|| this.ControlMode == SPControlMode.Display
|| this.ControlMode == SPControlMode.Invalid)

                return;

            base.CreateChildControls();

}
Добавим Javascript-функцию для Client-side валидации:
private const string c_ClientValidateFuncName = "CustomValidator1_ClientValidate";

  protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);

            // TODO: Клиенткая валидация должна возвращать True если регекс пустой
Page.ClientScript.RegisterClientScriptBlock(typeof(string),
"customvalidator1",

                @"<script type=""text/javascript"">
<!--
function " + c_ClientValidateFuncName + @"(ctrl, args) {
var value = args.Value;

                    if(value == null || value == '') {
var isRequired = " + this.Field.Required.ToString().ToLower() + @";
args.IsValid = !isRequired;
}
else if(value.match(/" + ((RegExField)this.Field).ValidationExpression + @"/)) {
args.IsValid = true;
} else {
args.IsValid = false;
}
}
//-->
</script>");
}
Добавим валидаторы в методе CreateChildControls():
protected override void CreateChildControls()
{
if (this.Field == null
|| this.ControlMode == SPControlMode.Display
|| this.ControlMode == SPControlMode.Invalid)
return;

            base.CreateChildControls();

            validator = new CustomValidator()
            {
                ID = "CustomValidator",
                ControlToValidate = "TextField",
                ClientValidationFunction = c_ClientValidateFuncName
            };

            string errorMessage = ((RegExField)this.Field).ErrorMessage;
validator.Text = "<br />" + Page.Server.HtmlEncode(errorMessage);
validator.ServerValidate += new ServerValidateEventHandler(validator_ServerValidate);

            Controls.Add(TemplateContainer.FindControl("TextField"));
Controls.Add(validator);
}
Добавление валидаторов приведет к размещению их в коллекции Controls, принадлежащей к Custom Field Control. Элементы ASCX-шаблона находятся в другой коллекции контролов  fieldControl.TemplateContainer.Controls.  Валидаторы ищут валидируемый контрол с помощью метода NamingContainer.FindControl(ControlToValidate). Данный метод работает, только если переместить валидируемый контрол в коллекциюControls, принадлежащую Custom Field Control.
Controls.Add(TemplateContainer.FindControl("TextField"));
Controls.Add(validator);
Зададим сообщение, которое будет показано в случае ошибки:
validator.Text = "<br />" + Page.Server.HtmlEncode(errorMessage);
Привяжем обработчик события ServerValidate. При этом логика client-sideвалидации должна совпадать с логикой ServerValidate.
validator.ServerValidate += new ServerValidateEventHandler(validator_ServerValidate);

void validator_ServerValidate(object source, ServerValidateEventArgs args)
{
args.IsValid = ((RegExField)this.Field).IsValueValid(args.Value);
}
public bool IsValueValid(string value)
{
if (String.IsNullOrEmpty(ValidationExpression))
return true;

            if (String.IsNullOrEmpty(value))
{
return !this.Required;
}
else
{
Match match = Regex.Match(value, ValidationExpression, RegexOptions.IgnoreCase);
return match.Success;
}
}
Как было сказано выше, этот способ работает, когда пользователь редактирует значения через веб-интерфейс, но не проверяются значения передаваемые разработчиком в коде через API в обход веб-интерфейса.

4.1 Перегрузка методаGetValidatedString()

Перегрузим метод GetValidatedString() в CustomFieldClass. В этом коде используется базовая имплементация метода GetValidatedString() для предварительной проверки. Производить такую проверку не обязательно, в данном случае это сделано для примера.
public override string GetValidatedString(object value)
{
string input = base.GetValidatedString(value);

            if (!IsValueValid(input))
throw new SPFieldValidationException(ErrorMessage);

            return input;
}
В данном случае был использован ранее объявленный метод IsValueValid() введенный для использования одинаковой логики во всех проверках.
Как уже было сказано выше, таким способом проверяются любые изменения значения, как произведенные пользователем через веб-интерфейс, так разработчиком через API. Особенность этой проверки заключается в том, что она возможна только на стороне сервера.
Следуя вышеуказанному способу, был создан тип поля RegExField, использующий валидацию введенного значения с помощью регулярного выражения. Проверка проводится на стороне клиента, если значение вводится через веб-интерфейс, и на стороне сервера, если значение задается разработчиком в коде. Регулярное выражение можно менять, редактируя свойства столбца, созданного с помощью данного типа поля.

Создание Custom Field Value

5.1 Способы реализации Custom Field Value

Возможны следующие решения, которые отличаются лишь местом, в котором происходит сериализация и десериализация значения. В строку для сохранения в БД.

img
Рис.4 – Методы получения значения поля

Промежуточный класс может быть произвольным (наследник System.Object). Такой класс должен удовлетворять следующим требованиям: 1) иметь конструктор для десериализации из сохраненных данных; 2) иметь перегрузку метода ToString() возвращающую сериализованные данные в строку.
Например, для промежуточного класса MyValueдолжны быть созданы следующие члены:

Классы Field Value для стандартных полей, такие как, например SPFIeldLookupValue, попадают в эту категорию.

Если просмотреть код класса SPFieldMultiColumnValue через .NETReflector, станет очевидно, что члены этого класса хранятся в списке Value.m_columnsкак экземпляры System.String.

5.1 Сериализация в промежуточном классе

Создадим проект WSPBuilder и добавим в него новый CustomFieldType. Создадим или используем предоставленный из шаблона WSPBuilder класс, который будет служить в качестве Field Value.
В нашем случае выбран способ сохранения данных в экземплярах объектов типа FontInfo, которые сериализуются с помощью BinarySerializer. (Это не является традиционным способом, и сделано для примера.)
Объявим класс FontInfo следующим образом:
[Serializable]
public class FontInfo
{
public string Family { get; set; }
public double Size { get; set; }
public FontWeight Weight { get; set; }

        /// <summary>
/// Для класса используемого в качестве value в методах
/// GetFieldValue, GetFieldValueAsText
/// метод ToString() ДОЛЖЕН возвращать сериализованное значение,
/// которое можно превратить обратно в FontInfo:
/// </summary>
/// <returns></returns>
public override string ToString()
{
return FontSerializer.Serialize(this);
}

        /// <summary>
/// Для использования в методе RenderFieldForDisplay
/// </summary>
/// <returns></returns>
public string ToDisplayString() {
return String.Format(
"<span style=\"font-family: {0}; font-size: {1}; font-weight: {2}; background-color: white;\">"
+"Example</span> (Family: {0}, Size: {1}pt, Weight: {2})",
Family, Size, Weight);
}
}

    public enum FontWeight { Bold, Normal }
Создадим методы для сериализации и десериализации объектов FontInfo и вынесем их в отдельный класс FontSerializer.
public static class FontSerializer
{
public static string Serialize(FontInfo font)
{
BinaryFormatter formatter = new BinaryFormatter();
var stream = new MemoryStream();

try
{
formatter.Serialize(stream, font);
return Convert.ToBase64String(stream.ToArray());
}
catch (Exception ex)
{
throw new ApplicationException(
"Не удалось выполнить сериализацию объекта FontInfo",
ex);
}

}

        public static FontInfo Deserialize(string base64string)
{
BinaryFormatter formatter = new BinaryFormatter();

            try
{
byte[] bytes = Convert.FromBase64String(base64string);

                var stream = new MemoryStream(bytes);

                FontInfo font = (FontInfo)formatter.Deserialize(stream);
return font;
}
catch (Exception ex)
{
throw new ApplicationException(
"Не удалось выполнить десериализацию объекта FontInfo",
ex);
}
}
}
Для правильной сериализации, нужно чтобы прегрузка метода ToString() для экземпляра FontInfo возвращала сериализованную строку.
/// <summary>
/// Для класса используемого в качестве value в методах
/// GetFieldValue, GetFieldValueAsText
/// метод ToString() ДОЛЖЕН возвращать сериализованное значение,
/// которое можно превратить обратно в FontInfo:
/// </summary>
/// <returns></returns>
public override string ToString()
{
return FontSerializer.Serialize(this);
}
Следует обратить внимание, что метод ToString() должен возвращать тоже значение, что и метод GetFieldValueAsText(), поскольку SharePoint  в некоторых местах вместо GetFieldValueAsText() использует cast к типу string или явный вызов value.ToString().
Подключим FontInfo к Custom Field Class с помощью перегрузки двух методов: GetFieldValue(), GetFieldValueAsText()
public override object GetFieldValue(string value)
{
if (!String.IsNullOrEmpty(value))
{
return FontSerializer.Deserialize(value);
}
else
{
return null;
}
}
Часто, вызов FontSerializer заключают в конструктор FontInfo(string), но в этом примере решено этого не делать.
public override string GetFieldValueAsText(object value)
{
if (value is FontInfo)
{
return ((FontInfo)value).ToString();
}
else
{
return String.Empty;
}
}
Создадим визуальный контрол ASCX дляSPFieldMultiLineText. Текстовое поле ID="TextField" должно содержаться в ASCX для поддержки базового функционала текстового поля.
img
Закомментированная разметка отображает следующее, рисунок 5.
img
Рис.5 – Отображение закомментированной разметки
Перегрузим CreateChildControls() и выполним databinding для элементов управления:
protected override void CreateChildControls() {
base.CreateChildControls();

            var cntr = TemplateContainer;
if (cntr == null
|| this.Field == null
|| this.ControlMode == SPControlMode.Display
|| this.ControlMode == SPControlMode.Invalid)
return;

            lstFontFamily = cntr.FindControl("lstFontFamily") as ListBox;
            ddlFontSize = cntr.FindControl("ddlFontSize") as DropDownList;
            ddlFontWeight = cntr.FindControl("ddlFontWeight") as DropDownList;

            lstFontFamily.DataSource = FontData.GetFontFamilies();
            ddlFontSize.DataSource = FontData.GetFontSizes();
            ddlFontWeight.DataSource = Enum.GetNames(typeof(FontWeight));

            lstFontFamily.DataBind();
            ddlFontSize.DataBind();
            ddlFontWeight.DataBind();

FontInfo font = ItemFieldValue as FontInfo;
if (font != null) {
DisplayValue(font);
}
}
Добавим метод для синхронизации выбранных значений в элементах управления с сохраненным в БД выбором пользователя, последний раз изменившего элемент элемент.
private void DisplayValue(FontInfo font)
{
lstFontFamily.Items.FindByValue(font.Family).Selected = true;
ddlFontSize.Items.FindByValue(font.Size.ToString()).Selected = true;
ddlFontWeight.Items.FindByValue(font.Weight.ToString()).Selected = true;
}
Вызовем этот метод в конце CreateChildControls(). Сохраненные в БД значения автоматически передаются в переменную-член ItemFieldValue. При этом для десериализации вызывается метод GetFieldValue(), см. выше.
FontInfo font = ItemFieldValue as FontInfo;
if (font != null) {
DisplayValue(font);
}
Добавим метод ConstructValue для конструирования экземпляра FontInfo, содержащего выбранные в элементах управления значения. Это значение будет использоваться для сохранения в БД. Сериализацию произведет метод GetFieldValueAsText(), см выше.
private object ConstructValue()
{
try
{
return new FontInfo()
{
Family = lstFontFamily.SelectedValue,
Size = Double.Parse(ddlFontSize.SelectedValue),
Weight = (FontWeight)Enum.Parse(typeof(FontWeight),
ddlFontWeight.SelectedValue)
};
}
catch (Exception ex)
{
throw new ApplicationException(
"Ошибка при разборе выбранных значений",
ex);
}
}
Перегрузим свойствоValue в созданном классе CustomFieldControl.
Аксессор Set {} не имплементирован, т.к. в ходе отладки выяснилось, что свойство Value не используется SharePoint для передачи в Custom Field Control текущего значения полученного из БД. Значение поля передается в свойство FieldItemValue.
public override object Value {
get {
return ConstructValue();
}
set {
}
}
Перегрузим метод RenderFieldForDisplay() для правильной прорисовки значений поля на карточке элемента DispForm.aspx.
protected override void RenderFieldForDisplay(HtmlTextWriter output) {
// Manual output
FontInfo font = ItemFieldValue as FontInfo;
if (font != null)
output.Write(font.ToDisplayString());

            // Render DispForm Web Part with RenderPattern
//base.RenderFieldForDisplay(output);
}
Наконец, объявим Field type definition для поля FontInfo. Зададим статичную надпись вместо DisplayPattern для того чтобы скрыть длинную строку содержащую сериализованные данные отображаемую на странице AllItems.aspx для каждого элемента.
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
<FieldType>
<!--
Имя поля используемое при создании экземпляров в CAML'е -->
<Field Name="TypeName">FontField</Field>
<!--
Имя родительского поля (из стандартных/custom-полей) -->
<Field Name="ParentType">Note</Field>
<!--
Имя отображаемое в списке столбцов SharePoint -->
<Field Name="TypeDisplayName">Font</Field>
<!--
Имя отображаемое при создании нового столбца (radio button list) -->
<Field Name="TypeShortDescription">Font field</Field>
<!--
Может ли быть создано пользователем? -->
<Field Name="UserCreatable">TRUE</Field>
<!--
Five-part class name
(класс - 1 часть, сборка - 4 части)
(сборка берется из любой feature.xml) -->
<Field Name="FieldTypeClass">
FontField.Code.FontField,
FontField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=45da335850c91d44
</Field>

    <RenderPattern Name="DisplayPattern">
<Switch>
<Expr>
<Column />
</Expr>
<Case Value="">
<!--Значение пустое - ничего не выводим-->
</Case>
<Default>
<!--Значение не пустое - отрисовываем-->
<HTML><![CDATA[<div>[Click an item to view this value]</div>]]></HTML>
</Default>
</Switch>
</RenderPattern>

</FieldType>
</FieldTypes>

Базовый тип поля SPFieldMultiColumn

6.1 Наследование SPFieldMultiColumn

SPFieldMultiColumn на практике, можно назвать специальным, "полуфабрикатным" полем предназначенным для разработчиков. Оно не имеет своего собственного класса CustomFieldControl, который бы определял внешний вид.  
Создаем новый проект  с помощью шаблона WSPBuilder. Добавляем файл XML-определения нового поля fldtypes_CombinationField.xml.
<?xml version="1.0" encoding="utf-8" ?>
<FieldTypes>
<FieldType>
<!--
Имя поля используемое при создании экземпляров в CAML'е -->
<Field Name="TypeName">CombinationField</Field>
<!--
Имя родительского поля (из стандартных/custom-полей) -->
<Field Name="ParentType">MultiColumn</Field>
<!--
Имя отображаемое в списке столбцов SharePoint -->
<Field Name="TypeDisplayName">Combination</Field>
<!--
Имя отображаемое при создании нового столбца (radio button list) -->
<Field Name="TypeShortDescription">Combination of values from several lists</Field>
<!--
Может ли быть создано пользователем? -->
<Field Name="UserCreatable">TRUE</Field>
<!--
Five-part class name
(класс - 1 часть, сборка - 4 части)
(сборка берется из любой feature.xml) -->
<Field Name="FieldTypeClass">
CombinationField.Code.CombinationField,
CombinationField, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d819dd2fde60c90f
</Field>

    <RenderPattern Name="DisplayPattern">
<Switch>
<Expr>
<Column />
</Expr>
<Case Value="">
<!--Значение пустое - ничего не выводим-->
</Case>
<Default>
<!--Значение не пустое - отрисовываем-->
<HTML><![CDATA[<div>]]></HTML>
<Column SubColumnNumber="0" />
<HTML><![CDATA[; ]]></HTML>
<Column SubColumnNumber="1" />
<HTML><![CDATA[; ]]></HTML>
<Column SubColumnNumber="2" />
<HTML><![CDATA[</div>]]></HTML>
</Default>
</Switch>
</RenderPattern>

  </FieldType>
</FieldTypes>
Создаем класс CombinationField унаследованный от SPFieldMultiColumn. Имплементируем два стандартных конструктора вызывающие базовые конструкторы. 
public CombinationField(SPFieldCollection fields, string fieldName)
: base(fields, fieldName)
{

}

public CombinationField(SPFieldCollection fields, string typeName, string displayName)
: base(fields, typeName, displayName)
{

}
Создаем класс CombinationFieldControl унаследованный от класса BaseFieldControl. Используется класс BaseFieldControl, являющийся общим базовым классом для всех типов полей.
Привязываем Custom Field Control к классу CombinationField с помощью его свойства FieldRenderingControl.
public override BaseFieldControl FieldRenderingControl
{
get
{
var ctrl = new CombinationFieldControl();
ctrl.FieldName = this.InternalName;
return ctrl;
}
}

Создаем ASCX-шаблон CombinationField.ascx со следующим содержимым, рисунок 6:
img
Рис. 6 – Создание ASCX-шаблона
<%@ Control Language="C#" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
Namespace="Microsoft.SharePoint.WebControls" %>

<SharePoint:RenderingTemplate ID="CombinationField" runat="server">
<template>
<asp:DropDownList ID="ddlItems1" runat="server" />
<asp:DropDownList ID="ddlItems2" runat="server" />
<asp:DropDownList ID="ddlItems3" runat="server" />
</template>
</SharePoint:RenderingTemplate>
Связываем созданный шаблон к Custom Field Control, указывая ID шаблона в аксессоре get перегруженного свойства DefaultTemplateName
public class CombinationFieldControl : BaseFieldControl
{
private const string c_templateName = "CombinationField";

        protected override string DefaultTemplateName {
get { return c_templateName; }
}

}
Создадим переменные-члены для контролов в классе CombinationFieldControl:
private DropDownList ddlItems1;
private DropDownList ddlItems2;
private DropDownList ddlItems3;
Создадим и заполним данными выпадающие списки в CreateChildControls():
protected override void CreateChildControls() {
base.CreateChildControls();

            var cntr = TemplateContainer;
if (cntr == null
|| this.Field == null
|| this.ControlMode == SPControlMode.Display
|| this.ControlMode == SPControlMode.Invalid)
return;

            // Create controls
ddlItems1 = cntr.FindControl("ddlItems1") as DropDownList;
ddlItems2 = cntr.FindControl("ddlItems2") as DropDownList;
ddlItems3 = cntr.FindControl("ddlItems3") as DropDownList;

            // Bind them to their data ranges
BindControl(ddlItems1, "List", "Title");
BindControl(ddlItems2, "RegeExList", "Title");
BindControl(ddlItems3, "FontList", "Title");

            // Display currently selected values
var value = ItemFieldValue as CombinationFieldValue;
if (value != null)
DisplayValue(value);
}
Объявим вспомогательные методы DisplayValue() иBindControl(). Класс CombinationFieldValue будет представлять собой Custom FieldValue, см. ниже
private void BindControl(ListControl listControl, string listName, string dataTextField) {
try {
var list = SPContext.Current.Web.Lists[listName];
var items = list.Items.Cast<SPListItem>().OrderBy(i => i[dataTextField]);
listControl.DataSource = items;
listControl.DataTextField = dataTextField;
listControl.DataValueField = dataTextField;
listControl.DataBind();
}
catch (Exception ex) {
throw new ApplicationException(
"Невозможно получить элементы для заполнения dropdown-списка", ex);
}
}

        private void DisplayValue(CombinationFieldValue value) {

            try {
ddlItems1.SelectedValue = value.Binding1.ItemTitle;
}
catch { }
try {
ddlItems2.SelectedValue = value.Binding2.ItemTitle;
}
catch { }
try {
ddlItems3.SelectedValue = value.Binding3.ItemTitle;
}
catch { }
}
Создадим классы ListBinding, ListBindingSerializer и CombinationFieldValue, рисунок 7.
img
Рис. 7 – Диаграмма классов


CombinationFieldValue

Представляет собой custom field value. Наследует SPFieldMultiColumnValue, благодаря чему поддерживает колонки для вывода в RenderingPatterns

   Binding1, 2, 3

   Экземпляры хранимые по одному в каждой колонке в виде строки

   ItemTitle1, 2, 3

   Заголовки элементов. Дублируют заголовки записанные в соответствующих ListBinding'ах и добавлены лишь для поддержки вывода заголовков элементов хранящихся в колонках с помощью RenderingPatterns.

ListBinding

Представляет сбой привязку к элементу списка SharePoint. Хранит имя списка и имя элемента в виде строк-членов. Помечен как сериализуемый.

ListBindingSerializer

Класс, представляющий собой утилиту сериализации экземпляров ListBinding в строки и восстановления из строк.

Имлементируем класс CombinationFieldValue:
[Serializable]
public class CombinationFieldValue : SPFieldMultiColumnValue
{
private const int c_PropNumber = 6;

        public CombinationFieldValue()
: base(c_PropNumber)
{

        }

        public CombinationFieldValue(string value)
: base(value)
{

        }

        public string ItemTitle1 { get { return base[0]; } }
public string ItemTitle2 { get { return base[1]; } }
public string ItemTitle3 { get { return base[2]; } }

        public ListBinding Binding1
{
get { return ListBinding.GetBinding(base[3]); }
set {
base[0] = value.ItemTitle;
base[3] = value.ToString();
}
}
public ListBinding Binding2
{
get { return ListBinding.GetBinding(base[4]); }
set {
base[1] = value.ItemTitle;
base[4] = value.ToString();
}
}
public ListBinding Binding3
{
get { return ListBinding.GetBinding(base[5]); }
set {
base[2] = value.ItemTitle;
base[5] = value.ToString();
}
}
}
Имплементируем методы для сериализации в классе ListBindingSerializer:
internal static class ListBindingSerializer
{
public static string Serialize(ListBinding binding)
{
BinaryFormatter formatter = new BinaryFormatter();
var stream = new MemoryStream();

            try
{
formatter.Serialize(stream, binding);
return Convert.ToBase64String(stream.ToArray());
}
catch (Exception ex)
{
throw new ArgumentException(
"Ошибка сериализации ListBinding", ex);
}
}

        public static ListBinding Deserialize(string serializedBinding)
{
BinaryFormatter formatter = new BinaryFormatter();

            try
{
var bytes = Convert.FromBase64String(serializedBinding);
var stream = new MemoryStream(bytes);
return (ListBinding)formatter.Deserialize(stream);
}
catch (Exception ex)
{
throw new ArgumentException(
"Ошибка десериализации ListBinding", ex);
}
}
}
Имлементируем метод ListBinding.ToString:
public override string ToString() {
return ListBindingSerializer.Serialize(this);
}
Подключим класс CombinationFieldValue к Custom Field Control с помощью свойства Value:
public override object Value {
get {
return ConstructValue();
}
set {
}
}
Там же, добавим метод для конструирования экземпляра из выбранных пользователем значений выпадающих списков ConstructValue:
private CombinationFieldValue ConstructValue() {

            var value = new CombinationFieldValue()
{
Binding1 = new ListBinding()
{
ItemTitle = ddlItems1.SelectedValue,
ListName = "List"
},
Binding2 = new ListBinding()
{
ItemTitle = ddlItems2.SelectedValue,
ListName = "RegeExList"
},
Binding3 = new ListBinding()
{
ItemTitle = ddlItems3.SelectedValue,
ListName = "FontList3"
}
};

            return value;
}
Подключим класс CombinationFieldValue к класу Custom Field Class с помощью методов GetFieldValue() и GetFieldValueAsText()
public override object GetFieldValue(string value)
{
if (!String.IsNullOrEmpty(value))
{
CombinationFieldValue obj = new CombinationFieldValue(value);
return obj;
}
else
{
return null;
}
}
public override string GetFieldValueAsText(object value)
{
if (value is CombinationFieldValue)
{
return value.ToString();
}
else
{
return String.Empty;
}
}
Следуя указанным действиям, был создан Custom Field Type на базе SPFieldMultiColumn, предоставляющий пользователю возможность задать значение с помощью трех выпадающих полей. Существует возможность модификации представленного решения таким образом, чтобы данные брались из произвольных источников, например списков SharePoint или базы данных SQL.

Борис Василенко,
младший разработчик компании ИТТИЛАН