Развертывание полей таксономии в SharePoint

Поля таксономии, также известные как поля управляемых метаданных (managed metadata), появились в SharePoint 2010. Метаданные позволяют создавать иерархии терминов, которые можно использовать как справочные значения (c typeahead в UI) и, начиная с SharePoint 2013, для навигации.

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

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

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

Проблема первая – схема поля

Если вы погуглите, то найдете минимум четыре статьи как деплоить таксономические поля:

Все рецепты приведут к неверному результату. Ближе всех к правде оказался последний пост, но и в нем есть проблемы.

Ни в одном посте не указано как поле попадает в список. А именно от этого зависит как поле будет работать. В прошлом посте я писал, что методы полей срабатывают, только если используется ContenTypeRef в схеме List Definition. Эти методы выполняют много работы – добавляют поля в список, привязывают event receiver_ы для синхронизации значений полей таксономии и catchall поля.

Итак правильная схема таксономического поля:

  <Field
       ID="{defbf0ed-377a-4e62-a980-0493ac0ef42e}"
       Name="TaxonomyColumn"
       DisplayName="Taxonomy Column"
       Type="TaxonomyFieldType"
       Required="FALSE"
       Group="Custom Site Columns"
       DisplaceOnUpgrade="TRUE" 
       Overwrite="TRUE"
  >
  </Field>

И все УлыбкаНет, это не шутка.

Если нужны множественные значения, то указать Mult=”TRUE” и Type="TaxonomyFieldTypeMulti".

Далее необходимо поле добавить в тип контента, а тип контента в список. Как писал в прошлом посте. Сделаете по-другому – не заработает.

На что стоит обратить внимание:

  • TaxonomyFieldType унаследован от Lookup, но атрибут ShowField нельзя указывать в схеме, это делается при добавлении поля в список. Если очень хочется указать, то ShowField=”Term$Resources:core,Language” и только так.
  • Не надо указывать Id связанного текстового поля. В случае когда Id не указан – генерируется автоматически.
  • Связанное поле деплоить не надо. Скорее всего вы это сделаете неправильно и у вас перестанет работать поиск. Кстати алгоритм создания связанного поля в 2010 и 2013 отличается.
  • В элементе Customizations можно указать атрибуты поля по-умолчанию. Это проще всего сделать поправив свойства поля на сайте, а потом аккуратно скопировав в схему.

К сожалению на этом месте возникает вторая проблема.

Проблема вторая – привязка к набору терминов

Мало задеплоить поле, надо еще привязать его к набору терминов. Для этого у поля нужно задать два свойства SspId и TermSetId, соответственно Id службы управляемых метаданных и Id набора терминов. Эти два идентификатора не являются инвариантными, то есть могут быть разными в разных фермах. Имена групп и наборов терминов тоже могут варьироваться в зависимости от текущего языка.

По сути есть два сценария развертывания:

TermSet уровня коллекции сайтов

Для каждой коллекции сайтов в хранилище Managed Metadata создается отдельная группа, видимая только в пределах этой коллекции сайтов. При при активации фичи с полем можно создавать (или импортировать) TermSet  в локальную группу, а при деактивации – удалять.

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    var site = properties.Feature.Parent as SPSite;
    var web = site.RootWeb;
    var field = web.Fields[FieldId] as TaxonomyField;

    var session = new TaxonomySession(site);

    var store = session.DefaultSiteCollectionTermStore;
    //sp2010
    //var group = store.Groups 
    //                 .OfType<Group>()
    //                 .First(g => g.IsSiteCollectionGroup);

    //sp2013
    var group = store.GetSiteCollectionGroup(site);

    bool allTermsAdded;
    string errorMessage;
    var termSet = store.GetImportManager()
                       .ImportTermSet(group, 
                                      textReader, 
                                      out allTermsAdded, 
                                      out errorMessage);
    if (!allTermsAdded)
    {
        termSet.Delete();
        store.CommitAll();
        throw new InvalidOperationException(errorMessage);
    }
            
    field.SspId = store.Id;
    field.TermSetId = termSet.Id;
    field.Update(true);
}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    var site = properties.Feature.Parent as SPSite;
    var web = site.RootWeb;
    var field = web.Fields[FieldId] as TaxonomyField;

    var session = new TaxonomySession(site);

    var store = session.DefaultSiteCollectionTermStore;
    store.GetTermSet(field.TermSetId).Delete();
    store.CommitAll();
}


Это самый простой метод. Вместо импорта можете использовать любой способ создания набора терминов.

Недостатков у этого метода два:

  • Нельзя создавать наборы терминов с фиксированными Id, так как фича может быть активирована на нескольких сайтах, а Id в рамках одной службы метаданных должны быть уникальны.
  • Набор терминов виден только в коллекции сайтов и не может быть использован в других коллекциях, например для XSP.

Внимание! Код выше не является referenece-кодом, его нельзя бездумно копипастить в свои решения. Для того чтобы он работал надо как минимум добавить логирование и проверку, что поле не настроено. FeatureActivated вызывается как минимум один раз для каждого экземпляра фичи, может быть и больше.

TermSet уровня службы

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

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    var site = properties.Feature.Parent as SPSite;
    var web = site.RootWeb;
    var field = web.Fields[FieldId] as TaxonomyField;

    var termStoreId = new Guid((string)site.WebApplication.Properties[field.InternalName + ".SspId"]);
    var termSetId = new Guid((string)site.WebApplication.Properties[field.InternalName + ".TermSetId"]);

    field.SspId = termStoreId;
    field.TermSetId = termSetId;
    field.Update(true);
}

Осталась только указать нужные SspId и TermSetId при установке WSP до активации фичи.  Проще всего это сделать в PowerShell скрипте установки. Причем скриптом можно создать свое приложение-службу таксономии и полностью перенести все термы из среды разработки. Это легко сделать с помощью командлетов Export-SPMetadataWebServicePartitionData и Import-SPMetadataWebServicePartitionData.

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

Заключение

Как и в прошлом посте, в этом я выкладываю правила для SPCAF (http://www.spcaf.com/) , которые помогут контролировать правильность развертывания таксономии.

PS. Кто заинтересовался как устроена таксономия внутри предлагаю почитать http://www.andrewconnell.com/sharepoint-2010-managed-metadata-in-depth-look-into-the-taxonomy-parts



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 не получится.