Поля таксономии, также известные как поля управляемых метаданных (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

Теги : SharePoint 2013, spcaf, taxonomy, provision, sp2013, managed metadata, SharePoint