3 правила создания списков SharePoint

Как ни странно, но на просторах интернета крайне мало информации как правильно делать списки и библиотеки SharePoint. В книгах об этом тоже редко пишут. Горькая правда заключается в том, что подавляющее большинство приложений содержит ошибки, связанные с развертыванием списков в SharePoint.

Чтобы устранить львиную долю этих ошибок, надо придерживаться следующих правил:

ContentType

Если вы хотите создать список, то не надо лезть в меню Add –> New Item –> List.  Для начала создайте поля для списка и тип контента. Даже если вы думаете, что тип контента будет ровно в одном списке, то все равно создавайте тип контента.

Тип контента вместе с полями деплойте в фиче уровня Site, желательно скрытой, чтобы никто не смог просто так отключить и поломать решение.

В этой же фиче необходимо выполнить все привязки Workflow, различных Policy и форм.

Важно чтобы после активации фичи тип контента был готов к использованию.

ContentTypeBinding

Элемент ContentTypeBinding позволяет привязать тип контента к экземпляру списка. При этом нет необходимости создавать List Definition. Достаточно создать список из одного из стандартных шаблонов, а потом сделать привязку.

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <ListInstance Title="List1"
                OnQuickLaunch="TRUE"
                TemplateType="100"
                FeatureId="00bfea71-de22-43b2-a848-c05709900100"
                Url="Lists/List1"
                Description="My List Instance">
  </ListInstance>
  <ContentTypeBinding 
      ContentTypeId="0x0100EDFEDEA571A241FD80430F4D48A91346" 
      ListUrl="Lists/List1"/>
</Elements>

ContentTypeBinding еще очень хорош тем, что даже при наличии типа контента в списке, он добавит недостающие поля, Workflow, политики. Это очень удобно при апгрейде.

Деплоить вместе с экземпляром списка в фиче уровня Web, иногда бывает и Site, но нечасто. В коде активации фичи обычно требуется удалить тип контента по-умолчанию, добавить представления, настроить параметры списка итд.

Необходимость писать код – один из недостатков привязки. Другой недостаток в том, что для нескольких списков код придется копипастить. Это все усложняет поддержку. Иногда проще сделать List Definition.

ContentTypeRef

Несколько раз подумайте перед тем как сделать List Definition (он же ListTemplate). Это очень опасный артефакт. Он крайне сложен в поддержке (сотни строк XML в самых простых случаях). Если у вас будет список, дефинишен которого вы удалили, то многие функции на сайте поломаются. При этом пользователи смогут самостоятельно создавать списки из дефинишенов через UI.

Если же вас необходим именно дефинишен, то сделайте его скрытым. Вы сильно снизите “область поражения”.

Но самая главная проблема в том, что List Definition, генерируемый Visual Studio, некорректен.

Поля определенные в List Definition не вызывают методов OnAdded и OnAddedToContentType. Типы контента (ContentType), определенные в дефинишене, не переносят все свойства, например обработчики событий.

Единственный способ правильно сделать List Definition – использовать только ContentTypeRef. Кроме того, используя ContentTypeRef, можно не указывать Fields, оставить пустой элемент. SharePoint автоматически добавит поля всех типов контента и вызовет их методы.

  <MetaData>
    <ContentTypes>
        <ContentTypeRef ID="0x0100EDFEDEA571A241FD80430F4D48A91346"/>
        <ContentTypeRef ID="0x0120" />
    </ContentTypes>
    <Fields></Fields>
    <Views> ... </Views>
    <Forms> ... </Forms>
  </MetaData>



UPD. Не убирайте тип контента папки (0x0120), он необходим для корректной работы.

Вместо заключения

Избежать многих ошибок при разработке под SharePoint поможет SPCAF (http://www.spcaf.com/). Для него можно создавать кастомные правила. Я сделал три правила, которые помогут правильно создавать списки.

Инструкция по сборке и установке http://docs.spcaf.com/v4/SPCAF_OVERVIEW_500_SDK.html



Обновление SharePoint app на TypeScript

В марте я писал про то, как разрабатывать приложения для SharePoint c помощью TypeScript. С тех пор прошло почти полгода, появились новые версии компилятора TypeScript (не совместимые со старыми) и улучшились описания типов для SharePoint (http://sptypescript.codeplex.com). Настало время обновить пример.

Пример приложения

Приложение позволяет фиксировать часы на рабочем месте.

image

Приложение ведет список всех временных интервалов, зафиксированных нажатием кнопок check-in\check-out. Пользователю отображается сумма всех его часов.

Также есть app part с тем же функционалом, но доступный для размещения на любой странице сайта.

Скачать можно по ссылке - TimeTrackerApp v0.9

Подготовка

Для начала необходимо:

Для того чтобы при сборке проекта выполнялась компиляция  TypeScript необходимо добавить в .csproj файл следующие элементы:

<PropertyGroup>
    <TypeScriptTarget>ES3</TypeScriptTarget>
    <TypeScriptIncludeComments>true</TypeScriptIncludeComments>
    <TypeScriptSourceMap>true</TypeScriptSourceMap>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" />

Библиотеки и определения

Прошлый раз я использовал библиотеки jQuery и knockoutjs с плагинами. В этот раз решил обойтись стандартными средствами SharePoint и небольшим хелпером из проекта sptypescript.

Чтобы все заработало необходимо добавить NuGet пакет sharepoint.TypeScript.DefinitelyTyped (http://www.nuget.org/packages/sharepoint.TypeScript.DefinitelyTyped/). Далее необходимо скопировать файл typescripttemplates.ts в проект, при необходимости поправить ссылку на sharepoint.d.ts.

Представление списка

Прошлый раз интерфейс был сделан на knockoutjs с асинхронной загрузкой данных. Это вызывало заметный лаг между отображением страницы и появлением интерфейса. Такое поведение очень сильно не нравилось и хотелось сделать лучше.

В новой версии интерфейс сделан на основе представления списка. Представление генерирует данные прямо в разметку страницы, и не требуется асинхронная загрузка. Кроме того, представление формирует разметку до рисования страницы. Для пользователя все выглядит, как-будто рисование происходит на сервере.

Для создания представления я воспользовался инструментами visual studio, но запрос пришлось вручную написать (незначимые детали убрал):

<View BaseViewID="2" Hidden="TRUE" >
  <ViewFields>
    <FieldRef Name="DurationInHours" />
    <FieldRef Name="ID" />
  </ViewFields>
  <Query>
    <Where>
      <Eq>
        <FieldRef Name="Author"/>
        <Value Type="Integer">
          <UserID/>
        </Value>
      </Eq>
    </Where>
  </Query> 
  <JSLink>~site/scripts/typescripttemplates.js|~site/scripts/view.js</JSLink>
</View>

Это представление получает все элементы автора.

Для того, чтобы поместить представление на страницу необходимо написать следующий код в Elements.xml

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="Pages">
    <File Path="Pages\Default.aspx" Url="Pages/Default.aspx" ReplaceContent="TRUE" >
      <View BaseViewID="2" List="Log" WebPartZoneID="full"  />
    </File>
  </Module>
</Elements>

Достаточно чтобы на странице Default.aspx была размещена зона веб-частей с идентификатором full.

Client-side rendering код представления:

CSR
.override(10000, 2)
.onPreRender(/* omitted for clarity */)
.header(' ')
.body(renderTemplate)
.footer(' ')
.onPostRender(initializeModel)
.onPostRender(suppressDefault)
.register();

Хелпер в файле typescripttemplates.ts реализует fluent-итерфейс для шаблонов  клиентского рендеринга.

Как оказалось нельзя просто так взять и переопределить шаблон View у представления, потому что он вызывается два раза. Если хочется сформировать полностью свою разметку, то надо переопределить body, header, footer, а еще написать код, который скрывает элемент для переключения страниц.

Еще один недостаток переопределения представления списка – при клике на представление появляется ribbon.

От обоих недостатков избавляет функция suppressDefault:

function suppressDefault(ctx: SPClientTemplates.RenderContext_InView) {
    var wpzoneCell = $get('MSOZoneCell_WebPart' + ctx.wpq);
    wpzoneCell.onkeyup = wpzoneCell.onmouseup = function () { };

    var footer = $get('scriptPaging' + ctx.wpq);
    footer.style.display = 'none';
}

Формирование разметки

Для удобства работы я сделал расширение контекста рендеринга:

interface TimeTrackerView extends SPClientTemplates.RenderContext_InView {
    totalHours: number;
    openLogItem: number;
    buttonText: string;
    spanId: string;
    buttonId: string;
}

Функция в onPreRender получает необходимые данные:

(ctx: TimeTrackerView) => {
    var rows = ctx.ListData.Row;
    ctx.totalHours = 0;
    for (var i = 0; i < rows.length; i++) {
        if (rows[i]['DurationInHours.']) {
            ctx.totalHours += parseFloat(rows[i]['DurationInHours.']);
        } else {
            ctx.openLogItem = rows[i]['ID'];
        }
    }
    ctx.buttonText = checkInOut(Boolean(ctx.openLogItem));
    ctx.spanId = ctx.wpq + '_totalHours';
    ctx.buttonId = ctx.wpq + '_button';
}

Переменная ctx.wpq имеет уникальное значение для каждого представления на странице и отлично подходит для формирования id для элементов.

Обращение к полю 'DurationInHours.' (с точкой в конце) это не опечатка. SharePoint генерирует два поля – одно без точки, форматированное с учетом локали сервера (для вывода), а второе с точкой, не форматированное. Такое происходит для многих полей, надо смотреть в отладчике что отдаёт SharePoint.

Сама функция создания разметки выглядит так:

function renderTemplate(ctx: TimeTrackerView): string {
    var result: string[] = [];
    result.push('<div>');
    result.push(String.format('<p>Total hours submitted <span id="{0}" >{1}</span></p>', ctx.spanId, ctx.totalHours.toPrecision(2)));
    result.push(String.format('<button id="{0}" disabled="disabled">{1}</button>', ctx.buttonId, ctx.buttonText));
    result.push('</div>');
    return result.join('');
}

Передача данных через контекст рендеринга – рекомендуемый способ при создании сложных шаблонов.

Простая функция выбора текста кнопки:

function checkInOut(checkedIn: boolean): string {
    return checkedIn ? 'Check-Out' : 'Check-In';
}

 

Логика интерфейса

При использовании client-side rendering только в onPostRender появляется возможность получить dom элементы, но это происходит до события загрузки страницы и до загрузки асинхронных скриптов, поэтому очень важно пользоваться механизмами Script On Demand, о которых я писал недавно.

SP.SOD.executeFunc('sp.js', 'SP.CleintContext', () => {
    var button = $get(ctx.buttonId);
    var span = $get(ctx.spanId);
    var totalHours = ctx.totalHours;

    var context = SP.ClientContext.get_current();
    var web = context.get_web();
    var list = web.get_lists().getById(ctx.listName);
    var currentItem: SP.ListItem;

    if (ctx.openLogItem) {
        currentItem = list.getItemById(ctx.openLogItem);
        context.load(currentItem);

        executeQuery(() => { button.disabled = false; });
    } else {
        button.disabled = false;
    }
    //...
})

Этот код инициализирует переменные для работы интерфейса, если есть открытый интервал, то он загружается с сервера и только после этого кнопка становится активной.

Функция executeQuery:

function executeQuery(callback: () => void) {
    context.executeQueryAsync(
        () => {
            callback();
        },
        (sender, args) => {
            alert(args.get_message());
            SP.Utilities.Utility.logCustomAppError(context,
                args.get_message() + '\n' + args.get_stackTrace());
            context.executeQueryAsync();
        });
}

В случае ошибки сообщение попадает в ULS и лог ошибок приложения.

Обработчик события нажатия кнопки (используя mQuery):

SP.SOD.executeFunc('mQuery.js', 'm$', () => {
    m$(button).click(e => {
        button.disabled = true;

        if (currentItem) {
            checkOut(updateView);
        } else {
            checkIn(updateView);
        }
        e.preventDefault();
    });
});

Функция updateView, обновляет представление:

function updateView() {
    button.disabled = false;
    button.innerHTML = checkInOut(Boolean(currentItem));
    span.innerHTML = totalHours.toPrecision(2);
};

Функции checkIn и checkOut, которые реализуют логику создания нового элемента в списке и “закрытие” существующего,  мало изменились с прошлого раза:

function checkOut(complete: () => void) {
    var startedDate = <Date>currentItem.get_item('StartDate');
    var dateCompleted = new Date();
    var hours = (dateCompleted.getTime() - startedDate.getTime()) / (1000 * 60 * 60);

    currentItem.set_item('DateCompleted', dateCompleted);
    currentItem.set_item('DurationInHours', hours);
    currentItem.update();

    SPAnimationUtility.BasicAnimator.FadeOut(span);

    executeQuery(() => {
        currentItem = null;
        totalHours += hours;
        SPAnimationUtility.BasicAnimator.FadeIn(span);
        complete();
    });
}

function checkIn(complete: () => void) {
    var item = list.addItem(new SP.ListItemCreationInformation());
    item.set_item('StartDate', new Date());
    item.update();
    executeQuery(() => {
        currentItem = item;
        complete();
    });
}

 

Добавление app part

После добавления app part в проект появляется еще одна aspx страница.

На ней необходимо добавить разметку в body:

<form runat="server">
    <WebPartPages:SPWebPartManager runat="server"/>
    <WebPartPages:WebPartZone runat="server" FrameType="None" ID="full">
    </WebPartPages:WebPartZone>
</form>

Чтобы заработали все скрипты необходимо в head добавить элемент:

<SharePoint:ScriptLink runat="server"/>

После этого в elements.xml надо добавить строчки, как и для главной страницы:

<File Path="Pages\CheckInOutWebPart.aspx" Url="Pages/CheckInOutWebPart.aspx" ReplaceContent="TRUE" >
  <View BaseViewID="2" List="Log" WebPartZoneID="full"  />
</File>

Заключение

Обязательно скачайте код по ссылке - TimeTrackerApp v0.9, пишите на TypeScript, используйте наши дефинишены (https://sptypescript.codeplex.com/).



Обновление SPTypeScript

Вчера была опубликовано обновление для SharePoint TypeScript Definitions. Новую версию определений можно получить через NuGet:

http://www.nuget.org/packages/sharepoint.TypeScript.DefinitelyTyped/

Или командой в Package Manager

PM> Install-Package sharepoint.TypeScript.DefinitelyTyped

Что нового

Анимация

В SharePoint 2013 добавили анимацию и, как всегда забыли, выложить документацию по этому делу. Я раскопал как работает анимация. К сожалению возможности библиотеки очень ограничены. Анимация работает для следующих атрибутов элементов:

  • Позиция (x,y)
  • Размеры (ширина, высота)
  • Прозрачность

Есть два способа вызвать анимацию.

Простой:

SPAnimationUtility.BasicAnimator.FadeOut(element); 
SPAnimationUtility.BasicAnimator.FadeIn(element);
SPAnimationUtility.BasicAnimator.Resize(element, width, height);
SPAnimationUtility.BasicAnimator.Move(element, x, y);

И чуть более сложный:

var state = new SPAnimation.State();
state.SetAttribute(SPAnimation.Attribute.Opacity, 0.2);
var animation = new SPAnimation.Object(
                        SPAnimation.ID.Basic_Opacity, 
                        500,  /*duration*/
                        element, 
                        state);

animation.RunAnimation(); 

Подробное описание можно посмотреть по ссылке - SPAnimation.d.ts

А также в проект добавлен пример: Animation.ts

За этим простым API лежит довольно сложная реализация, в которой я пока еще не разобрался. В будущем, возможно, будет расширено описание типов анимации и добавится примеров использования.

RequestExecutor

Это класс, позволяющий выполнять Ajax запросы с синтаксисом, похожим на jQuery:

var re = new SP.RequestExecutor(targetSiteUrl); 
re.executeAsync({     
    url: targetUrl,     
    method: 'GET',     
    success: function(response) {         
        //console.log(response.body);         
        //do stuff     
    }
});

Кроме обычных Ajax запросов этот класс позволяет делать кросс-доменные запросы в модели приложений для SharePoint 2013.

Ранее я писал об этом классе в посте SharePoint и Ajax.

Исправленное и дополненное описание SOD

В обновление вошли также последние версии определений для Script On Demand в SharePoint. Возможности SOD я описывал в предыдущем посте: SharePoint Script On Demand.

 

На этом на сегодня все. Пишите код на TypeScript, качайте дефинишены, оставляйте фидбек на сайте проекта.



Многоликий SharePoint

Я часто консультирую людей по поводу SharePoint, и каждый раз я слышу примерно одно и то же: “мы планируем\разрабатываем\внедряем\используем\хотим_выкинуть портал на SharePoint”. Такое ощущение, что единственное для чего предназначен SharePoint – делать порталы. Как битрикс или, не дай бог, друпал. Ситуацию еще подогревают HRы, которые хотят “интранеты” (имея ввиду ровно тоже, что и порталы).

Если же попадается грамотный заказчик,который понимает проблемы за пределами фразы “корпоративный портал”, то подрядчик зачастую пытается впарить свое “портальное решение”. Это “решение” должно закрывать все потребности заказчика одним махом.

Конечно порталы каждый раз разные, но в них очень много общего.

Типовой корпоративный портал на SharePoint

Обычно на портал возлагаются следующие задачи:

  • Портал обязательно должен иметь уникальный дизайн .
  • Портал должен способствовать улучшению коммуникации.
  • Портал должен иметь функцию документооборота.
  • Портал должен быть хранилищем активов для подразделений.
  • Портал должен обеспечивать совместную работу.
  • ..а еще автоматизировать заявки, делать поиск, обязательно нужна мини-crm, голосования, показывать дни рождения и трудовые юбилеи, новости, фотографии, ссылки… ничего не забыл?

Самое странное, что многие цели противоречат друг другу, но это никого не смущает. Например документооборот крайне плохо сочетается с уникальным дизайном. Никому в голову не придет делать уникальный дизайн в Documentum или DocsVision, а в SharePoint это нормально. Хранилище активов требует разграничения доступа в соответствии с организационной структурой, а совместная работа и улучшение коммуникаций, наоборот, требует преодоления рамок организационной структуры. Весь feature soup, который предлагается в качестве полезного функционала, вообще ни как не коррелирует с заявленными целями и присутствует, в основном, “для галочки”. И это еще не самое страшное…

Самое страшное начинается когда этот портал пытаются реально использовать. Ведь бизнес динамичен, постоянно требуется что-то поменять, подкрутить, дать доступ, создать сайт, изменить страницу. Но универсальное “портальное решение” больше напоминает не гибкую систему, а изваяние из гранита, которое даже передвинуть нельзя, не то что как-то поменять.

Приспособить “портал” под конкретные задачи пользователей оказывается сложно, а без этого adoption становится низкий и вложения не окупаются. Гигантский набор фич на портале используется от силы на 20% и, зачастую, страдает от проблем масштабирования.

Почему так происходит

Ситуация описанная выше повторяется чуть менее, чем всегда. Казалось бы зачем покупать порталы, когда они приводят к неудовлетворительному результату. Но проблема в том, что все продают “корпоративные порталы”. Все это не только Microsoft и партнеры, это в том числе те, кто внедряет другие технологии, от вики движков, до битриксов и WebSphere. Тут работает принцип социального доказательства: “если все остальные покупают корпоративные порталы, то чем мы хуже”.

Сам термин корпоративный портал является ментальным шорткатом, который избавляет продавца и покупателя от необходимости выяснения потребностей. Кроме того, для продавца продать "корпоративный портал” – это хороший способ продать проект допиливания портала под конкретные нужды.

Я думаю для многих не секрет, что некоторые вендоры “корпоративных порталов” по факту не имеют портала как продукта (распространяющегося без программистов).

Как сделать правильное решение

Для создания хорошего решения на SharePoint необходимо:

  1. Сосредоточиться на целях
  2. Поставить измеримые KPI
  3. Планировать информационную архитектуру
  4. Создавать самые простые решения

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

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

Примеры решений на SharePoint

Корпоративный поиск

Цель – уменьшить время на поиск информации.
KPI – количество успешных запросов (с кликом на результат) и количество неуспешных (без результатов или без клика на результат). На основании этих величин можно получить экономический эффект и вычислить целевые значения.
Архитектура – искать “везде” обычно плохо работает. Смотрите на поисковую выдачу интернет-поисковиков. Там есть разные типы информации, поиск по разным источникам, продвигаемые результаты,actionable результаты. Адаптируйте к своим источникам и внедряйте.
Решение – стандартный поиск SharePoint 2013. После внедрения поисковая аналитика и “тюнинг” поиска.

Рабочие области команд и проектов

Цель – увеличить эффективность совместной работы.
KPI – количество пересылаемых по электронной почте документов. Взять baseline до запуска системы, уменьшить на 30%-50%-80% по вкусу.
Архитектура – необходимо сделать максимально простой процедуру создания сайтов с библиотекой и списками для совместной работы. Без заявок в ИТ и согласований.
Решение – стандартные сайты команд SharePoint, плоские разрешения, контролируемый жизненный цикл. Обучение пользователей возможностям платформы.

Корпоративный сайт

Цель – сделать единую точку входа для информационных ресурсов компании.
KPI – Количество просмотренных страниц сайта (новостей, событий, фото\видео материалов), количество опубликованных, процент сотрудников, ответивших на опросы. Каждая новость, опубликованная на сайте, должна быть прочитана значительным количеством сотрудников (30% и более), при определенном темпе публикации можно получить довольно точные количественные характеристики.
Архитектура – фиксированные типы информации, малое количество редакторов, простая система разрешений. Участие пользователей – лайки и комментарии, ответы на опросы.
Решение – отдельная коллекция сайтов с режимом публикации, кастомный дизайн, фиксированная структура. Обязательное обучение авторов контента.

Документооборот

Цель – уменьшить время согласования.
KPI – Время согласования документов (по типам). Можно взять baseline с текущей ситуации, уменьшить на 30%-50%-80% по вкусу.
Архитектура – фиксированные маршруты, роли, типы документов. Это очень важный момент. Если в согласовании может принимать участие произвольное количество людей, то считать KPI становится невозможным. Если документы нельзя разделить по типам, то тоже вызывает проблемы расчета KPI.
Решение – Отдельный сайт или коллекция, типы контента, шаблоны, workflow. Предусмотреть вычисление количества дней\часов\минут на согласование. Ну и без обучения, скорее всего, никуда.

Специализированные варианты решений:

  • Enterprise Content Management \ Электронный Архив
  • Автоматизация заявок
  • Базы знаний
  • Системы оценки персонала
  • Системы внутренних вакансий

Попробуйте на досуге продумать конкретные цели и KPI таких систем.

Самое важное – все это разные системы. Нет смысла все валить в одну кучу и называть “корпоративным порталом”, так достичь KPI не получится.



SharePoint Script On Demand

В SharePoint 2013 довольно богатая клиентская библиотека. Ранее я уже писал об аналоге jQuery и об Ajax библиотеках. Одна из важнейших частей для разработки масштабного JavaScipt приложения – загрузчик скриптов. Такое тоже есть в SharePoint.

Почему необходимо управлять загрузкой скриптов

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

Но важнейшая задача управления загрузкой скриптов – создание полноценной модульности для javascript. Ведь сам язык не предоставляет инструмента описания связей между модулями и гарантий, что зависимости будут загружены до выполнения скрипта.

Обе причины особенно важны в SharePoint. Страницы SharePoint тяжелые, и лучше загружать скрипты как можно позже. Сами же страницы строятся из кучи независимых компонент (созданных разными разработчиками), поэтому модульность скриптов важна как нигде.

В мире веб-разработки фактически стандарт для управляемой загрузки скриптов – библиотека requirejs. Но в ней есть критический недостаток, её придумал не Microsoft, поэтому в SharePoint своя библиотек для загрузки скриптов. Называется она Script On Demand.

Script On Demand

Вся библиотека содержится в классе SP.SOD в файле init.js. Script On Demand спроектирован как неинтрузивная библиотека, в отличие от requirejs, то есть скрипты будут работать и без нее. Это важно в App Parts, которые по сути отдельные страницы в iframe. Там нет смысла откладывать загрузку скриптов.

Документация по SP.SOD есть на MSDN, но она там, мягко говоря, ужасная. В проекте SPTypeScript есть качественные определения типов для SP.SOD, они будут более полезны (SP.Init.d.ts).

Добавление скриптов на страницу

Есть серверный контрол ScriptLink, который позволяет указать в разметке aspx страницы скрипт. По умолчанию скрит будет загружаться сразу же, но асинхронно, то есть не будет блокировать рисование страницы.

У контрола ScriptLink несколько атрибутов:

  • Name – указывает имя скрипта. Если указать только имя файла, то скрипт будет загружаться из _layouts. Поддерживаются url tokens, такие как ~site и ~sitecollection.
  • Localizable – если стоит значение true и указано только имя скрипта, то скрипт будет за загружаться из _layouts/{langId}.
  • LoadAfterUI – если стоит значение true, скрипт будет добавлен в конец страницы (независимо от местоположения контрола ScriptLink) и загрузка начнется после рисования всего интерфейса.
  • OnDemand – если указано значение false или атрибут отсутствует, то генерируется такая разметка:
    <script type="text/javascript">
    // <![CDATA[
    document.write(
        '<script type="text/javascript" src="script.js"></' 
        + 'script>');
    // ]]>
    </script>
    
    
    Если же указано значение true, то генерируемый код такой:
    <script type="text/javascript">
    RegisterSod("script.js", "script.js");
    </script>
    
    
    Метод RegisterSod это SP.SOD.registerSod. Загрузка такого скрипта не начнется, пока не будет вызвана явно с помощью класса SP.SOD.

Если вы делаете серверную веб-часть или контрол без разметки, то вам могут пригодиться статические методы класса ScriptLink:

К сожалению класс ScriptLink не доступен в Sandbox, поэтому обязательно необходимо пользоваться классом SP.SOD.

Внимание: никогда в веб-частях и контролах не генерируйте тег script с атрибутом src. Ваш контрол или веб-часть могут быть добавлены более одного раза на страницу и скрипты будут загружаться несколько раз. Если же вы вызовете SP.SOD.registerSod несколько раз, то скрипт будет загружен только один раз (свойство идемпотентности).

SP.SOD.registerSod(fileName: string, url: string): void;

В первом параметре указывается имя зависимости, а во втором – url скрипта.

Ожидание загрузки скрипта

Если вы воспользовались ScriptLink с атрибутом OnDemand=false, и вам надо дождаться окончания его загрузки, чтобы вызвать свой код, то вам нужен метод

SP.SOD.executeOrDelayUntilScriptLoaded(func: () => void , depScriptFileName: string): bool

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

Вызов этой функции часто нужен для ожидания окончания загрузки файла sp.js, содержащего JSOM.

SP.SOD.executeOrDelayUntilScriptLoaded(function() {
    var ctx = SP.Context.gt_current();
    //Do something
}, 'sp.js');

Увы, эта функция теряет this.

Загрузка по требованию

Если вам необходимо загрузить скрипт по требованию, то для этого есть целых два метода:

  • SP.SOD.execute(fileName: string, functionName: string, args?: any[]): void;
  • SP.SOD.executeFunc(fileName: string, typeName: string, fn: () => void ): void;

Пример вызовов (результат одинаковый):

SP.SOD.executeFunc('mQuery.js', 'm$', function() {
     m$.ready(function (){ });
}

SP.SOD.execute('mQuery.js', 'm$.ready', [function (){ }] );


Есть еще один метод, который загружает сразу несколько скриптов:
SP.SOD.loadMultiple(keys: string[], fn: () => void): void;

В отличие от предыдущих методов не проверяет наличие объекта\типа\функции. Но необходимость в этом методе небольшая, так как есть зависимости между скриптами.

Зависимости между скриптами

Чтобы сделать зависимость между скриптами надо вызвать метод:

SP.SOD.registerSodDep(fileName: string, dependentFileName: string): void;

В дальнейшем, если вызвать загрузка скрипта с именем fileName, то перед ним будет загружен скрипт с именем dependentFileName. Эта функция идемпотентна, как и SP.SOD.registerSod.

Использование SOD в своих скриптах

Чтобы загружать свой скриптовый файл с помощью SOD, надо в конце скрипта вызывать метод

SP.SOD.notifyScriptLoadedAndExecuteWaitingJobs(scriptFileName: string): void;

Так как SOD неинтрузивная библиотека, то хорошим тоном будет писать так:

if ( SP && SP.SOD ) {
    SP.SOD.notifyScriptLoadedAndExecuteWaitingJobs("script.js");
}

Если вы используете внешние скрипты и хотите их подключить к SOD, то потребуется написать хелпер (typescript):

function loadExternalScript(key: string, scriptPath: string) {
    var scriptNode = document.createElement('script');
    scriptNode.type = 'text/javascript';
    scriptNode.src = scriptPath;

    var headNode = document.getElementsByTagName('head');
    if (headNode[0] != null)
        headNode[0].appendChild(scriptNode);

    var callback = (e: Event) => {
        SP.SOD.notifyScriptLoadedAndExecuteWaitingJobs(key);
    };
    scriptNode.onreadystatechange = callback;
    scriptNode.onload = callback;
}

SOD и MDS

SharePoint 2013 принес еще одно новшество- Minimal Download Strategy. Эта стратегия заставляет браузер перезагружать не всю страницу, а частями. Такая загрузка снижает трафик, но плохо влияет на скрипты. Потому что теперь страница может быть обновлена без перезагрузки скриптов.

Для того, чтобы функция была вызвана после ajax перезагрузки страницы (без перезагрузки скриптов), надо зарегистрировать её с помощью

function RegisterModuleInit(scriptFileName: string, initFunc: () => void ): void;

Первый параметр – url скрипта, указанный в SOD, второй – функция, которую надо вызывать при ajax перезагрузке.

Ковыряя исходники скриптов SharePoint, я нашел интересную вещь. Функция SP.SOD.notifyScriptLoadedAndExecuteWaitingJobs сама вызывает RegisterModuleInit. В качестве initFunc передается функция с именем $_global_!scriptName!. !scriptName! формируется путем отрезания расширения (после последней точки) и заменой всех точек на символы подчеркивания. Но это работает только для скриптов в _layouts/15.

Publish\Subscribe

В модульных приложениях на JavaScript часто используется механизм подписок и оповещений для взаимодействия между компонентами приложения. В SOD используется такой механизм и он выставлен наружу, так что можно его использовать в своем приложении.

Publish\Subscribe состоит из трех методов:

  • SP.SOD.notifyEventAndExecuteWaitingJobs(eventName: string, args?: any[]): void;
    Этот метод вызывает события с указанными параметрами.
  • SP.SOD.executeOrDelayUntilEventNotified(func: Function, eventName: string): bool;
    Эта функция создает подписку на событие с параметрами и вызывается если событие уже наступило.
  • SP.SOD.delayUntilEventNotified(func: Function, eventName: string): void;
    Эта функция только создает подписку.

Заключение

Обязательно используйте SOD в своих проектах. Это позволит делать модульные скрипты и корректно взаимодействовать со скриптами SharePoint.