Pull to refresh

Структурируем вложения к элементам списка в Sharepoint 2010

Reading time 19 min
Views 12K
Доброго всем дня!

В Sharepoint 2010 присутствует множество стандартных типов столбцов сайта, но как минимум два важных, на мой взгляд, отсутствуют. Возможно, разработчики оставили «поле» для расширений от сторонних компаний. Но мы «сами с усами» =)

Отсутствующими «из коробки» являются столбцы типа время и типа файл. Для столбца типа дата можно выбрать либо дату со временем, либо одну дату. А вот только время в столбце хранить нельзя. Реализация элемента управления для редактирования двух выпадающих списков с возможными значениями для часов и минут не представляется сильно интересной задачей. Поэтому под катом реализация поля типа файл — SPFileField, которую можно использовать и как краткую инструкцию по созданию собственных типов столбцов для Sharepoint 2010. Решение позволяет добавлять к спискам Sharepoint столбцы нового типа и загружать в них файлы через автоматически генерируемые формы. При этом файлы и информация о них «раскладываются» по разным полям:


В общем виде задача выглядит так: пользователь хочет создавать элементы списка и прикладывать к ним файлы, а приложенные файлы нужно разложить по различным столбцам в зависимости от их назначения (например, техническое задание, договор оплаты, реквизиты и т.д.) и отобразить ссылки для их загрузки другим пользователям. Идея реализации такая: хранить все файлы в библиотеке документов, а в списке — только ссылку на нужный файл из библиотеки. Пользователю показывать ссылку для скачивания/удаления/замены файла (если он есть) и форму для добавления нового файла в библиотеку документов. При сохранении обрабатывать изменения, сделанные на клиенте, и, в случае необходимости, обновлять библиотеку документов.

Взглянув сейчас на набор задействованных в данной реализации языков и технологий, не может не радовать то, что все они представлены в Visual Studio. Для создания полностью работающего варианта потребовалось описание типа столбца на XML, кодирование логики поля и связанного контрола на C#, клиентской логики на JavaScript, описание внешнего вида контрола на ASCX и его стиля на CSS, а также пара тригерров, опять же, на C#. Теперь обо всём по порядку…

Описание типа столбца на XML


Для описания собственного типа столбца, первое, что необходимо сделать (после создания проекта, естественно) — это добавить в проект Sharepoint Mapped Folder под названием XML. Сделать это можно из контекстного меню проекта (папка XML находится в папке Template):

Сразу же можно добавить и следующие директории: Template/CONTROLTEMPLATES, Template/LAYOUTS/STYLES, Template/LAYOUTS/XSL. Вот что у меня получилось:

Для того чтобы определить новый тип столбца Sharepoint необходимо добавить в папку XML новый файл соответствующего расширения. В этом файле задаются свойства нового типа, такие как имя типа столбца, путь к классу, описывающему поведение, список дополнительных свойств, которые может задавать пользователь при создании столбца, и т.д.
fldtypes_SPFileField.xml
<?xml version="1.0" encoding="utf-8" ?>
<!-- Начинаем описание списка типов столбцов, которые присутствуют в данном проекте -->
<FieldTypes>
  <!-- Описываем столбец -->
  <FieldType>
    <!--Имя столбца используемое при создании экземпляров в CAML-->
    <Field Name="TypeName">SPFileField</Field>
    <!-- Имя родительского столбца (из стандартных/custom-полей) -->
    <Field Name="ParentType">Text</Field>
    <!-- Имя отображаемое в списке столбцов SharePoint -->
    <Field Name="TypeDisplayName">SPFileField</Field>
    <!-- Имя отображаемое при создании нового столбца -->
    <Field Name="TypeShortDescription">SPFileField</Field>
    <!-- Может быть создано  пользователем -->
    <Field Name="UserCreatable">TRUE</Field>
    <!--Задаём области, из которых доступно создание поля -->
    <Field Name="ShowOnListCreate">TRUE</Field>
    <Field Name="ShowOnSurveyCreate">TRUE</Field>
    <Field Name="ShowOnDocumentLibraryCreate">TRUE</Field>
    <Field Name="ShowOnColumnTemplateCreate">TRUE</Field>
    <!--Увы, не нашёл как повлиять на отображаемое имя в списках фильтрации и сортировки-->
    <Field Name="Sortable">FALSE</Field>
    <Field Name="Filterable">FALSE</Field>
    <!-- Если клиентсткое приложение не сможет правильно отобразить наше поле, то оно будет отображено как базовый тип (Text) -->
    <Field Name="AllowBaseTypeRendering">TRUE</Field>  
    <!-- Имя класс (до запятой) и сборка -->
    <Field Name="FieldTypeClass">SPFileFieldControl.SPFileField, $SharePoint.Project.AssemblyFullName$</Field>
    <!-- Свойства, которые могут быть заданы для описываемого типа. --> 
    <PropertySchema>
      <Fields>
        <!-- В нашем случае это имя библиотеки, которую будем использовать как хранилище файлов, и имя поля, в котором будем хранить имя файла -->
        <Field Name="LibraryName" DisplayName="Имя библиотеки" Type="Text" Required="TRUE" />
      </Fields>
    </PropertySchema>
  </FieldType>
</FieldTypes>

Подробнее можно посмотреть здесь и далее по ссылкам. Для тестирования создадим список (TestList) и библиотеку документов (TestLibrary). После развёртывания решения в его текущем виде к списку TestList (впрочем, как и к любому другому) можно будет добавить столбец SPFileField:

Активировать ничего не надо, так как кастомный тип за фичу (Feature) не считается. Если вдруг новый тип столбца не появляется, перестартуйте IIS. После добавления столбца SPFileField вылезет ошибка, так как в сборке не найден указанный нами тип SPFileFieldControl.SPFileField. Исправим это, добавив в проект файл SPFileField.cs, который будет содержать описание класса нового столбца (SPFileField), класса значения (FileValue) и контрола (SPFileFieldControl), отвечающего за отображение и редактирование.

Серверная логика на C#


FileValue наследуется от SPFieldMultiColumnValue и программируется таким образом, чтобы хранить 3 свойства: имя файла, путь к файлу и уникальный ИД файла. SPFieldMultiColumnValue позволяет хранить эти значения в одной строке через разделитель ';#' и предоставляет доступ к ним через индексатор. Код скрыт под спойлером, но здесь и далее достаточно неплохо прокомментирован.
FileValue
    public class FileValue : SPFieldMultiColumnValue
    {
        private const int c_PropNumber = 3;

        public FileValue() : base(c_PropNumber) {}

        public FileValue(string value) : base(value) {}

        public string Name
        {
            get { return base[0]; }
            set { base[0] = value; }
        }

        public string Url
        {
            get { return base[1]; }
            set { base[1] = value; }
        }

        public Guid _UniqueID = Guid.Empty;
        public Guid UniqueID
        {
            get
            {
                if (_UniqueID == Guid.Empty)
                    _UniqueID = new Guid(base[2]);
                return _UniqueID;
            }
            set
            {
                _UniqueID = value;
                base[2] = value.ToString();
            }
        }
    }

SPFileField наследуется от SPField, который является базовым для столбцов списков Sharepoint. В классе SPFileField мы реализуем необходимые для родительского класса конструкторы, получаем описанное выше свойство LibraryName и используем его при создании SPFileFieldControl, который будет отвечать за отображение контрола на формах просмотра и редактирования.
SPFileField
    public class SPFileField : SPField 
    {
        // Реализуем необходимые для SPField конструкторы
        public SPFileField(SPFieldCollection fields, string fieldName) : base(fields, fieldName) { }

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

        public override BaseFieldControl FieldRenderingControl
        {
            get
            {
                // Получаем задаваемые пользователем свойства
                string libraryName = Convert.ToString(this.GetCustomProperty("LibraryName"));

                // Создаём и возвращаем контрол для работы с полем
                BaseFieldControl ctrl = new SPFileFieldControl(libraryName);
                ctrl.FieldName = this.InternalName;
                return ctrl;
            }
        }

        // По строке получаем значение FileValue
        public override object GetFieldValue(string value)
        {
            if (String.IsNullOrEmpty(value))
                return null;

            return new FileValue(value);
        }
    }

И, наконец, третий класс в файле SPFileField.cs — это SPFileFieldControl, описывающий логику работы по загрузке файлов. Данный класс наследуется от BaseFieldControl. SPFileFieldControl содержит описание имён шаблонов, которые необходимо использовать для отрисовки поля на формах отображения и редактирования. В функциях SetupEditTemplateControls и SetupDisplayTemplateControls описана настройка шаблонов в зависимости от того был ли уже загружен файл. Чтобы не ограничиваться одним столбцом нового типа SPFileField на список в функции SetupEditTemplateControls динамически подцепляется JavaScript для удаления и добавления файлов, в котором используются идентификаторы контролов, уходящие на клиентскую машину.

Здесь же располагается логика по обработке пользовательских изменений. Описывается она в перегруженном методе UpdateFieldValueInItem и заключается в следующем: если файл был загружен (поле ItemFieldValue задано), но скрытое поле, содержащее его UniqueID, пустое, то связанный элемент из библиотеки документов будет удалён. Также связанный элемент удаляется при загрузке нового файла. Изначально были попытки заменить файл, не удаляя элемент библиотеки, но это не привело к успеху, так как поле тип файла (File_x0020_Type) для библиотеки документов не обновляется при перезаписи файлов через метод Add с параметром overwrite=true.

Так как в одной библиотеке документов Sharepoint не может содержаться два документа с одинаковыми названиями (а требовать от наших пользователей вводить уникальные имена не имеет смысла ;)), то в качестве имени нового документа генерируется Guid. После добавления имя заменяется («для красоты») на «выданный» при создании идентификатор элемента (ID) плюс имя файла, полученное от клиента. В итоге получается примерно следующее: «3-Имя файла.docx». Когда файл добавлен, информация о нём сохраняется в нашем поле (оно находится в исходном списке TestList, к которому мы добавили столбец типа SPFileField).
SPFileFieldControl
    public class SPFileFieldControl : BaseFieldControl 
    {
        // Сообщение, которое выводим в случае отсутствия файла
        private const string fileNotExist = "Файл не загружен";

        // Имя шаблона для формы редактирования
        private const string editTemplateName = "SPFileFieldControlEdit";

        // Имя шаблона для формы отображения
        private const string displayTemplateName = "SPFileFieldControlDisplay";

        // Имя библиотеки, где храним файлы
        public string libraryName;

        // Переопределяем свойство Value, так чтобы работать с FileValue
        private FileValue currentFile = null;
        public override object Value
        {
            get
            {
                return currentFile;
            }
            set
            {
                currentFile = (FileValue)value;
            }
        }

        public SPFileFieldControl(string aLibraryName)
        {
            libraryName = aLibraryName;
        }

        // Получаем значение текущего файла
        protected override void OnInit(EventArgs e)
        {
            currentFile = (FileValue)this.ItemFieldValue;
            base.OnInit(e);
        }

        // Подключаем шаблоны, которые будут использоваться для отрисовки
        protected override string DefaultTemplateName
        {
            get
            {
                return base.ControlMode == SPControlMode.Display
                           ? displayTemplateName
                           : editTemplateName;
            }
        }

        public override string DisplayTemplateName
        {
            get { return displayTemplateName; }
        }

        // После создания контролов по заданному шаблону задаём некоторые их свойства
        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            if (base.ControlMode == SPControlMode.Display)
            {
                SetupDisplayTemplateControls();
            }
            else
            {
                SetupEditTemplateControls();
            }
        }

        // Настраиваем контролы для отображения поля на форме редактирования
        private void SetupEditTemplateControls()
        {
            FileUpload fuDocument = (FileUpload)TemplateContainer.FindControl("fuDocument");
            HyperLink aFile = (HyperLink)TemplateContainer.FindControl("aFile");
            HiddenField hdFileName = (HiddenField)TemplateContainer.FindControl("hdFileName");
            HtmlInputImage btnDelete = (HtmlInputImage)TemplateContainer.FindControl("btnDelete");
            HtmlInputImage btnAdd = (HtmlInputImage)TemplateContainer.FindControl("btnAdd");

            // В зависимости от того загружен ли файл отображаем контролы
            if (currentFile != null)
            {
                // Получаем ссылку для загрузки файла
                SPWeb web = SPContext.Current.Site.RootWeb;
                SPList list = web.GetList(SPUrlUtility.CombineUrl(web.ServerRelativeUrl, libraryName));
                SPListItem spFileItem = list.GetItemByUniqueId(currentFile.UniqueID);

                // Показываем ссылку на файл
                aFile.NavigateUrl = SPUrlUtility.CombineUrl(web.ServerRelativeUrl, spFileItem.Url);
                aFile.Text = currentFile.Name;

                // А в скрытом поле сохраняем UniqueId файла
                hdFileName.Value = spFileItem.UniqueId.ToString();
            }
            else
            {
                // Отображаем сообщение о том, что файл не задан
                aFile.Text = fileNotExist;
                hdFileName.Value = String.Empty;
            }

            btnDelete.Attributes.Add("onclick", String.Format(@"clearFileValue('{0}','{1}','{2}','{3}');return false;",
                                                              aFile.ClientID, fuDocument.ClientID, hdFileName.ClientID,
                                                              fileNotExist));
            btnAdd.Attributes.Add("onclick", String.Format(@"changeDisplay('{0}');return false;", fuDocument.ClientID));
            fuDocument.Attributes.Add("onchange",String.Format(@"changeFileName(this,'{0}');return false;", aFile.ClientID));
        }

        // Настраиваем контролы для отображения поля на форме отображения
        private void SetupDisplayTemplateControls()
        {
            if (currentFile != null)
            {
                // Получаем ссылку для загрузки файла
                SPWeb web = SPContext.Current.Site.RootWeb;
                SPList list = web.GetList(SPUrlUtility.CombineUrl(web.ServerRelativeUrl, libraryName));
                SPListItem spFileItem = list.GetItemByUniqueId(currentFile.UniqueID);

                // Показываем ссылку на файл
                HyperLink aFile = (HyperLink)TemplateContainer.FindControl("aFile");
                aFile.NavigateUrl = SPUrlUtility.CombineUrl(web.ServerRelativeUrl, spFileItem.Url);
                aFile.Text = currentFile.Name;
            }
        }

        // Обновляем поле после изменений пользователя
        public override void UpdateFieldValueInItem()
        {
            // Проверяем валидность страницы
            Page.Validate();
            if (Page.IsValid)
            {
                // Посмотрим, что пришло с клиента
                FileUpload fuDocument = (FileUpload)TemplateContainer.FindControl("fuDocument");
                HiddenField hdFileName = (HiddenField)TemplateContainer.FindControl("hdFileName");

                // Если скрытое поле пусто и есть файл, значит, его нужно удалить
                if (hdFileName.Value == String.Empty && currentFile != null)
                {
                    // Удаляем файл
                    deleteFile();

                    // Обнуляем значение поля
                    currentFile = null;
                }

                // Загружаем файл и выставляем на него ссылки
                if (fuDocument.HasFile)
                {
                    SPWeb web = SPContext.Current.Site.RootWeb;
                    SPFolder folder = web.GetFolder(SPUrlUtility.CombineUrl(web.ServerRelativeUrl, libraryName));

                    // Если файл был, удалим его
                    if (currentFile != null)
                    {
                        deleteFile();
                    }

                    // Добавим новый файл и сохраним соответствующие значения
                    currentFile = addFile(folder, fuDocument);
                }

                base.UpdateFieldValueInItem();
            }
        }

        // Удаляем файл
        private void deleteFile()
        {
            // Получаем элемент из библиотеки документов по UniqueID и удаляем его
            SPWeb web = SPContext.Current.Site.RootWeb;
            SPList list = web.GetList(SPUrlUtility.CombineUrl(web.ServerRelativeUrl, libraryName));
            SPListItem spFileItem = list.GetItemByUniqueId(currentFile.UniqueID);
            spFileItem.Delete();
        }

        // Добавляем файл
        private FileValue addFile(SPFolder folder, FileUpload fuDocument)
        {
            string uniqueName = Guid.NewGuid().ToString();
            // Добавляем файл с именем uniqueName
            SPFile spfile = folder.Files.Add(uniqueName, fuDocument.FileContent,true);

            // Обновляем имя нового элемента библиотеки документов
            spfile.Item["BaseName"] = String.Format("{1}-{0}", fuDocument.FileName, spfile.Item.ID);
            spfile.Item.Update();

            return new FileValue()
            {
                Url = spfile.Item.Url,
                UniqueID = spfile.Item.UniqueId,
                Name = fuDocument.FileName
            };
        }
    }

Описание ASCX шаблона


Полученное решение позволяет успешно создавать столбцы сайта нового типа. Однако формы пока не показываются, так как Sharepoint не может обнаружить указанные в классе SPFileFieldControl шаблоны отображения (RenderingTemplate). Описываются они в ascx-файле в папке CONTROLTEMPLATES. Чтобы создать соответствующий файл нужно добавить в проект пользовательский контрол (User Control) и удалить автоматически сгенерированные файлы с расширениями ascx.cs и ascx.designer.cs. Файл описывает два шаблона: шаблон для формы редактирования SPFileFieldControlEdit, состоящий из гиперссылки на файл, поля для загрузки нового файла и кнопок добавить/удалить, и шаблона для формы отображения SPFileFieldControlDisplay, который содержит одну гиперссылку на файл.
SPFileFieldControl.ascx
  <%-- Шаблон для формы редактирования --%>
  <SharePoint:RenderingTemplate ID="SPFileFieldControlEdit" runat="server">
    <Template>
        <%-- Подключаем CSS и JS --%>
        <SharePoint:CssRegistration ID="CssRegistration1" name="/_layouts/styles/FileFieldControl.css"  after="corev4.css" runat="server"/>
        <SharePoint:ScriptLink ID="ScriptLink1" runat="server" Name="FileFieldControl.js" Localizable="false"/>
        <%-- Гиперссылка для загрузки --%>
        <asp:HyperLink ID="aFile" runat="server" CssClass="ffc-hl"  EnableViewState="False"/>
        <%-- Кнопка удалить - очищает гиперссылку и скрытое поле) --%>
        <input type="image" ID="btnDelete" runat="server" src="/_layouts/images/DELETE.gif" alt="delete"/>
        <%-- Кнопка добавить - показывает FileUpload --%>
        <input type="image" ID="btnAdd" runat="server" src="/_layouts/images/newrowheader.png" alt="add"/>
        <br/>
        <asp:FileUpload ID="fuDocument" runat="server" CssClass="ffc-fu" />     
        <%-- В скрытом поле храним UniqueId чтобы в случае необходимо удалить файл --%>
        <asp:HiddenField runat="server" ID="hdFileName"/>
    </Template>
  </SharePoint:RenderingTemplate>
  <%-- Шаблон для формы отображения --%>
  <SharePoint:RenderingTemplate ID="SPFileFieldControlDisplay" runat="server">
    <Template>
        <%-- Подключаем CSS --%>
        <SharePoint:CssRegistration ID="CssRegistration1" name="/_layouts/styles/FileFieldControl.css"  after="corev4.css" runat="server"/>
        <%-- Гиперссылка для загрузки --%>
        <asp:HyperLink ID="aFile" runat="server" CssClass="ffc-hl"  EnableViewState="False"/>
    </Template>
  </SharePoint:RenderingTemplate>

Немного JavaScript и CSS


Чтобы решение начало работать, необходимо добавить в проект JavaScript файл (я добавил его в папку LAYOUTS) и описать в нём три функции: changeDisplay — отображение/скрытие элемента по ИД, clearFileValue — очистка ссылки на файл, скрытого поля и поля загрузки файла, и changeFileName для отображения имени загружаемого файла.
FileFieldControl.js
// Отображение/скрытие элемента по ИД
function changeDisplay(id) {
    var v = document.getElementById(id);
    if (v.style.display == 'block') //|| v.style.display == '') 
    {
        v.style.display = 'none';
    }
    else {
        v.style.display = 'block';
    }
}
// Очистка ссылки на файл, скрытого поля и поля загрузки файла
function clearFileValue(aID, fID, hfId, defText) {
    var a = document.getElementById(aID);
    a.innerText = defText;
    a.removeAttribute("href");

    var hf = document.getElementById(hfId);
    hf.value = '';

    var f = document.getElementById(fID);
    f.outerHTML = f.outerHTML;
}
// Обработка изменения файла
function changeFileName(oFile, aID) {
    var a = document.getElementById(aID);

    var fullPath = oFile.value;
    if (fullPath)
    {
        // В гиперсылке покажем имя файла без полного пути к нему
        var startIndex = (fullPath.indexOf('\\') >= 0 ? fullPath.lastIndexOf('\\') : fullPath.lastIndexOf('/'));
        var filename = fullPath.substring(startIndex);
        if (filename.indexOf('\\') === 0 || filename.indexOf('/') === 0) {
            filename = filename.substring(1);
        }
        
        a.innerText = filename;
    }
}

Ну и куда же без CSS! Добавим в папку STYLES файл FileFieldControl.css, в котором укажем выравнивание для гиперссылки и то, что по умолчанию FileUploader будет скрыт.
FileFieldControl.css
.ffc-hl {
    text-align:center;
    vertical-align:top;
}

.ffc-fu {
    margin-top: 3px;
    display: none;
}

Проверяем!


Стоп. Сначала стоит упомянуть про фичи). Я обнаружил всего одну — для списка, в который добавлены столбцы нового типа, должны быть запрещены вложения . Иначе стандартные формы, генерируемые Sharepoint, пытаются обработать контрол FileUpload и вылетает ошибка «Элемент списка был сохранен, но одно или несколько вложений сохранить не удалось».

Вот теперь проверяем! Напомню, что мы создали список TestList, добавили к нему столбец нового типа с названием TestFile, а также добавили библиотеку документов TestLibrary, в которую и должны, в конечном счете, попадать файлы. После всей проделанной работы форма для создания элементов TestList выглядит так:

При добавлении файла наш контрол принимает вид:

А так выглядят только что созданные элементы в списке TestList:

И в библиотеке TestLibrary:

Поле SPFileField на форме просмотра:

На форме редактирования:

Заметили небольшие кракозябры в представлении элементов списка? Правильно, так по умолчанию отображается описанное нами значение FileValue (имя файла, путь к нему и UniqueID через разделитель). Чтобы поправить это, необходимо обратиться к XSL.

XSL


Как вы уже наверно догадались файл XSL необходимо добавить в папку с соответствующим именем. Будьте осторожны — Visual Studio предлагает добавить XSLT файл после поиска по шаблону 'XSL'. Необходимо заменить расширения файла на то, которое нам необходимо. Также стоит отметить, что имя файла должно начинаться с 'fldtypes_'. Подробнее об этом можно почитать здесь. Там же можно найти стандартные заголовки, которые ниже были опущены. В самом XSL файле описывается разбор строки, которая через ';#' содержит имя и путь к файлу, и вывод результирующей гиперссылки.
fldtypes_SPFileField.xsl
  <!-- Этот шаблон будет использован для столбцов типа SPFileField-->
  <xsl:template match="FieldRef[@FieldType='SPFileField']" mode="Text_body">
    <!-- Получаем значение поля -->
    <xsl:param name="thisNode" select="."/>
    <xsl:variable name="full" select="$thisNode/@*[name()=current()/@Name]" />
    <!-- На первой позиции имя, на второй путь к файлу (на третьей UniqueID, который здесь не используется) -->
    <!-- "Вырезаем" имя и путь к файлу -->
    <xsl:variable name="name" select="substring-before(substring-after($full,';#'),';#')" />
    <xsl:variable name="url" select="substring-before(substring-after(substring-after($full,';#'),';#'),';#')" />
    <!-- Выводим гиперссылку для загрузки файла -->
    <xsl:element name="a">
      <xsl:attribute name="href">
        <xsl:value-of select="concat($RootSiteUrl,'/',$url)"/>
      </xsl:attribute>
      <xsl:value-of select="$name"/>
    </xsl:element>
  </xsl:template>

После развёртывания решения с новым XSL файлом, представление списка TestList примет более приличный вид:


EventReciever-ы


В получившемся решении не предусмотрено удаление файлов из TestLibrary при удалении элементов списка TestList. То есть если удалить только что созданные элементы ('без названия'), то файл 'Документ Microsoft Word.docx' останется в библиотеке TestLibrary. Поправить такое поведение можно с помощью EventReceiver-ов (кода, срабатывающего при наступлении определённого события). Для этого в проект добавим EventReceiver. Я назвал его ER_OnItemDeleting. Нас интересуют события для элементов списка, а именно момент удаления элемента (An item is being deleted), так как после удаления мы потеряем значения всех полей, а значит и идентификатор файла.

Нужный список, для которого необходимо обрабатывать события, у меня не появился, поэтому я выбрал любой и потом внёс исправления вручную. Для этого в файле Elements.xml (внутри ER_OnItemDeleting) у тега Receivers нужно заменить атрибут ListTemplateId на ListUrl со значением Lists/TestList. Привязывать EventReceiver к конкретному списку можно как с помощью ListTemplateId, так и с помощью ListUrl. Второе легче узнать, поэтому я использовал именно его. Получилось следующее описание EventReciever-a:
Elements.xml
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Receivers ListUrl="Lists/TestList">
      <Receiver>
        <Name>ER_OnItemDeletingItemDeleting</Name>
        <Type>ItemDeleting</Type>
        <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
        <Class>SPFileFieldControl.ER_OnItemDeleting.ER_OnItemDeleting</Class>
        <SequenceNumber>10000</SequenceNumber>
      </Receiver>
  </Receivers>
</Elements>

Кастомные триггеры считаются Sharepoint за отдельные фичи. Поэтому после добавления EventReceiver-а в проекте так же окажется Feature1. Чтобы в дальнейшем было легче ориентироваться я переназвал фичу 'Triggers', а в поле Title вбил 'SPFileFieldControl Triggers'. Сам код EventReceiver-а не сложен и заключается в том, что мы перебираем поля удаляемого элемента в попытке найти столбец типа SPFileField. Если поле найдено, и файл был загружен, то он удаляется.
ER_OnItemDeleting.cs
   public class ER_OnItemDeleting : SPItemEventReceiver
    {
        public const string ER_OnItemDeletingName =
            "SPFileFieldControl.ER_OnItemDeleting.ER_OnItemDeleting";

        public const string SPFileFieldName = "SPFileField";

        public override void ItemDeleting(SPItemEventProperties properties)
        {
            // Проходим по всем полям удаляемого элемента списка
            for (int i = 0; i < properties.ListItem.Fields.Count; i++)
            {
                // Если поле типа SPFileField
                if (properties.ListItem.Fields[i].TypeAsString == ER_OnItemDeleting.SPFileFieldName)
                {
                    // И был загружен файл
                    FileValue fileValue = properties.ListItem[properties.ListItem.Fields[i].StaticName] as FileValue;
                    if (fileValue == null)
                        continue;

                    // Удаляем его
                    SPFile spFile = properties.Web.GetFile(fileValue.UniqueID);
                    spFile.Delete();
                }
            }

            base.ItemDeleting(properties);
        }
    }

Опубликовав решение, можно убедиться, что файл из библиотеки TestLibrary действительно удаляется, когда это необходимо (фича должна активироваться автоматом при развёртывании решения). Но не подключать же вручную EventReceiver-ы при добавлении к списку столбца типа SPFileField! Оказывается, событие создания нового столбца можно также отследить и обработать с помощью EventReceiver-а. По аналогии с предыдущим создадим новый EventReceiver, отслеживающий события списка (List Events), такие как добавление и удаление столбцов (a field was added/a field is being removed). Вот так выглядит конечная иерархия файлов в проекте:

Разрабатывая EventReceiver-ы, бывает полезно посмотреть список всех имеющихся и ручками поправить, если что-то не так. Ниже приведён код, отрабатывающий во время добавления и удаления столбцов типа SPFileField. При добавлении столбца мы подключаем к целевому списку вышеописанный EventReceiver, который удаляет связанные файлы при удалении элементов списка. А при удалении — удаляем связанные файлы и, если в списке больше нет столбцов типа SPFileField, отменяем обработку события OnItemDeleting.
ER_OnFileFieldCRUD.cs
    public class ER_OnFileFieldCRUD : SPListEventReceiver
    {
        public const string SPFileFieldName = "SPFileField";

        public override void FieldAdded(SPListEventProperties properties)
        {
            // Если добавляется поле SPFileField, то подписываемся на удаление элементов, чтобы удалять связные файлы
            if (properties.Field.TypeDisplayName == SPFileFieldName)
            {
                // Пробуем добавить EventReceiver, если его нет
                bool eventReceiverExists = false;
                for (int i = 0; i < properties.List.EventReceivers.Count;i++)
                {
                    if (properties.List.EventReceivers[i].Class ==
                        ER_OnItemDeleting.ER_OnItemDeleting.ER_OnItemDeletingName)
                    {
                        eventReceiverExists = true;
                        break;
                    }
                }

                if (!eventReceiverExists)
                {
                    properties.List.EventReceivers.Add(SPEventReceiverType.ItemDeleting, 
                                                       Assembly.GetExecutingAssembly().FullName, 
                                                       ER_OnItemDeleting.ER_OnItemDeleting.ER_OnItemDeletingName);
                }
            }

            base.FieldAdded(properties);
        }

        public override void FieldDeleting(SPListEventProperties properties)
        {
            if (properties.Field.TypeDisplayName == SPFileFieldName)
            {
                // Если удаляется столбец SPFileField, то
                // 1) Удаляем все связные файлы, если они есть
                for (int i = 0; i < properties.List.Items.Count; i++)
                {
                    SPListItem splistItem = properties.List.Items[i];
                    FileValue fileValue = splistItem[properties.Field.StaticName] as FileValue;

                    if (fileValue == null)
                        continue;

                    SPFile spFile = properties.Web.GetFile(fileValue.UniqueID);
                    spFile.Delete();
                }

                // Проверяем, останутся ли после удаления столбцы типа SPFileField
                bool anyFileFieldExist = false;
                for (int i = 0; i < properties.List.Fields.Count; i++)
                {
                    if (properties.List.Fields[i].TypeAsString == SPFileFieldName
                        && properties.List.Fields[i].StaticName != properties.Field.StaticName)
                    {
                        anyFileFieldExist = true;
                        break;
                    }
                }

                // 2) Отписываемся от триггера, если больше нет колонок типа SPFileField
                if (!anyFileFieldExist)
                {
                    for (int i = 0; i < properties.List.EventReceivers.Count; )
                    {
                        if (properties.List.EventReceivers[i].Class == ER_OnItemDeleting.ER_OnItemDeleting.ER_OnItemDeletingName)
                        {
                            properties.List.EventReceivers[i].Delete();
                        }
                        else
                        {
                            i++;
                        }
                    }
                }
            }

            base.FieldDeleting(properties);
        }
    }

На этом я завершил разработку столбца SPFileField, хотя, как говорится, «совершенству нет предела». Например, можно обработать удаление файла из библиотеки документов напрямую (я эту библиотеку пользователям не показываю) и очищать ссылку на него (значение FileValue). Или добавить проверку существования библиотеки документов при создании столбцов SPFileField и т.д. Чтобы не ограничивать вас в ваших желаниях выкладываю исходники, а также готовое решение.

Заключение


Полученное решение позволяет создавать столбцы Sharepoint 2010 и «загружать» в них файлы. При этом загруженные файлы не являются стандартными вложениями, которые после загрузки различаются только по названию. Файлы, попавшие в систему через столбец SPFileField, обладают связью с конкретным столбцом, которую можно использовать при создании собственных View. Таким образом, представленный подход, позволяет разделять загрузку файлов и вывод информации о них на уровне столбцов.
Дополнительным плюсом является то, что мы не ограничены одним столбцом типа SPFileField на каждый список, и столбцы SPFileField могут быть настроены для загрузки файлов в разные библиотеки документов (может быть интересно показать пользователю и связанную библиотеку документов).

На этом у меня всё, спасибо за внимание!
Отдельное спасибо хочется сказать этому ресурсу, на котором достаточно подробно описано создание Сustom Field Types для Sharepoint 2007. Как показывает мой опыт, большая часть написанного справедлива и для Sharepoint 2010.
Tags:
Hubs:
+2
Comments 2
Comments Comments 2

Articles