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

Скажу сразу что под обработчиком событий компонента имеется ввиду feature receiver. Но так как этот блог на русском языке и я не знаю более менее удачного перевода – для заголовка статьи я решил использовать перевод из Msdn. Дальше по тексту я буду использовать термин фича, понятный всем Sharepoint разработчикам.

Провиженинг управляемых метаданных (managed metadata) – достаточно частое требование в проектах на Sharepoint 2010. Часто вместе с созданием сайт коллекции необходимо добавить начальные данные для упрощения задачи сотрудникам, ответственным за контент (напр. страницы публикаций, данные в lookup листах и др.). Управляемые метаданные также могут быть созданы при активации фичи во время провиженинга. Например, заказчик может предоставить вам метаданные в виде xml файла (либо в любом другом формате, который можно распарсить программно) и вам нужно создать иерархию таксономий на основе этого файла. Одна из возможностей – использовать стандартный механизм импорта из csv файла, доступный в Term store. Но при этом во-первых у вам еще один шаг ручной настройки, а во-вторых стандартный импорт ограничен в кастомизации, напр. вы не сможете установить переводы ваших метаданных, синонимы, а также смержить метаданные с существующими наборами терминов (term sets). В этом посте я покажу как программно установить таксономии с любым уровнем вложенности на основе xml файла.

Прежде всего нужно создать фичу с областью действия – сайт коллекция. Это удобно, т.к. мы сможем добавить ее в onet.xml кастомного определения сайта (в секцию<SiteFeatures>) – и управляемые метаданные будут созданы при провиженинге сайт коллекции. Но при этом нужно иметь ввиду, что метаданные могут быть изменены после того, как они были запровиженены. Поэтому если планируется активировать фичу, устанавливающую метаданные, несколько раз в течение периода существования сайт коллекции, нужно позаботиться о том, чтобы по ошибке не перезаписать изменения, сделанные вручную администраторами метаданных. Простой способ – проверить что группа или набор терминов с текущим названием уже существуют и не перезаписывать их. Более сложный – мержить наборы терминов. Но последний сценарий вне скопа данной статьи.

Вернемся к нашей фиче и ее feature.xml файлу:

<?xml version="1.0" encoding="utf-8" ?>
<Feature
    Id="2FCC2C0B-A5D9-480B-B15B-189B5755185B"
    Title="Managed metadata example"
    Description=""
    Version="1.0.0.0"
    Scope="Site"
    Hidden="FALSE"
    ReceiverAssembly="ManagedMetadataProgrammaticalExample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d71c4ad7a0705ced"
    ReceiverClass="ManagedMetadataProgrammaticalExample.TermSetsFeatureReceiver"
    xmlns="http://schemas.microsoft.com/sharepoint/">
    <ElementManifests>
        <ElementFile Location="DefaultMetadata.xml"/>
    </ElementManifests>
</Feature>

Мы определили обработчик событий фичи, в котором будет выполняться непосредственная работы, т.е. парсинг xml файла и создание таксономии в Managed metadata service application. Также мы добавили ссылку на файл DefaultMetadata.xml, который содержит управляемые метаданные. Напр. так моэет выглядеть метаданные для интернет магазина (для простоты используем небольшую выборку):

<?xml version="1.0" encoding="utf-8" ?>
<TermSets>
  <TermSet Name="Categories">
    <Term Name="Books">
      <Term Name="IT">
        <Term Name="Sharepoint" />
        <Term Name="ASP.Net MVC" />
        <Term Name=".Net" />
      </Term>
      <Term Name="Math">
        <Term Name="Algebra" />
        <Term Name="Geometry" />
        <Term Name="Math analysis" />
      </Term>
    </Term>
    <Term Name="Films">
      <Term Name="The Big Bang Theory" />
      <Term Name="Haus MD" />
      <Term Name="Numb3rs" />
    </Term>
  </TermSet>
</TermSets>

Хотя в примере выше только один набор терминов, код приведенный в статье будет работать также для многих наборов (т.е. если xml будет содержать несколько наборов – все будут созданы). Провиженинг метаданных производится в 2 шага:

  1. Парсинг xml и представление данных в объектной модели
  2. Создание наборов терминов и терминов

Нам понадобятся 2 DTO класса:

public class TermSetDTO
{
    public string Name { get; set; }
    public List<TermDTO> Terms { get; set; }
}

public class TermDTO
{
    public string Name { get; set; }
    public List<TermDTO> Terms { get; set; }
}

Как видно, это очень простое представление стандартных сущностей TermSet и Term.

Теперь обратимся к коду обработчика событий:

public class TermSetsFeatureReceiver : SPFeatureReceiver
{
    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        var site = properties.Feature.Parent as SPSite;
        if (site == null)
        {
            return;
        }

        SPSecurity.RunWithElevatedPrivileges(
            () =>
                {
                    using (var elevatedSite = new SPSite(site.ID, site.Zone))
                    {
                        string defaultMetadataFilePath = Path.Combine(properties.Feature.Definition.RootDirectory, "DefaultMetadata.xml");
                        var termSetsService = new TermSetsService();
                        termSetsService.CreateTermSetsFromXml(elevatedSite, defaultMetadataFilePath);
                    }
                });
    }
}

Важный момент: для определенности код исполняется под elevated privileges. Строго говоря, сами elevated privileges не нужны – нам нужно знать под каким аккаунтом исполняется код, чтобы добавить его в группу администраторов банка терминов  (Term store administrators). Поэтому если сайт коллекция создается из Central Administration, то необходимо добавить аккаунт пула приложений в IIS в группу администраторов банка терминов (Term store administrators) в Приложении службы метаданных (Managed metadata service application).

Код обработчика достаточно простой – он передает путь к DefaultMetadata.xml в экземпляр TermSetsService, который выполняет непосредственную работу (это полезный паттерн – по возможности отвязывать функциональность от Sharepoint артефактов):

public class TermSetsService
{
    private const string TERM_SET_TAG = "TermSet";
    private const string TERM_TAG = "Term";
    private const string NAME_ATTR = "Name";
    private const string GROUP_NAME = "Shop";
    private const int ENGLISH_LOCALE_ID = 1033;

    public void CreateTermSetsFromXml(SPSite site, string fileName)
    {
        TermStore termStore = null;
        try
        {
            var taxonomySession = new TaxonomySession(site);
            termStore = taxonomySession.DefaultKeywordsTermStore;

            if (termStore == null)
            {
                return;
            }

            if (!termStore.Groups.IsNullOrEmpty() && termStore.Groups.Any(g => g.Name == GROUP_NAME))
            {
                return;
            }

            var taxonomyGroup = termStore.CreateGroup(GROUP_NAME);
            if (taxonomyGroup == null)
            {
                return;
            }

            // load terms sets from xml file
            var termSets = this.load(fileName);
            if (termSets.IsNullOrEmpty())
            {
                return;
            }

            createTermSets(taxonomyGroup, termSets);

            termStore.CommitAll();
        }
        catch (Exception x)
        {
            if (termStore != null)
            {
                termStore.RollbackAll();
            }
            throw;
        }
    }

    private List<TermSetDTO> load(string fileName)
    {
        try
        {
            var result = new List<TermSetDTO>();

            XDocument doc = XDocument.Load(fileName);
            foreach (var termSetElement in doc.Descendants(TERM_SET_TAG))
            {
                var nameAttr = termSetElement.Attributes().FirstOrDefault(a => a.Name == NAME_ATTR);
                if (nameAttr == null)
                {
                    continue;
                }

                string termSetName = nameAttr.Value;

                var termSet = new TermSetDTO { Name = termSetName };
                termSet.Terms = this.getTerms(termSetElement);
                result.Add(termSet);
            }

            return result;
        }
        catch (Exception x)
        {
            return new List<TermSetDTO>();
        }
    }

    private List<TermDTO> getTerms(XElement e)
    {
        try
        {
            if (e == null)
            {
                return null;
            }

            var terms = new List<TermDTO>();
            foreach (var termElement in e.Elements(TERM_TAG))
            {
                var nameAttr = termElement.Attributes().FirstOrDefault(a => a.Name == NAME_ATTR);
                if (nameAttr == null)
                {
                    continue;
                }

                var term = new TermDTO {Name = nameAttr.Value};
                term.Terms = this.getTerms(termElement);
                terms.Add(term);
            }
            return terms;
        }
        catch (Exception x)
        {
            return new List<TermDTO>();
        }
    }

    private void createTermSets(Group taxonomyGroup, List<TermSetDTO> termSets)
    {
        if (taxonomyGroup == null)
        {
            return;
        }

        if (termSets == null)
        {
            return;
        }

        // create managed metadata term set for each term set dto
        foreach (var ts in termSets)
        {
            if (taxonomyGroup.TermSets.Any(t => t.Name == ts.Name))
            {
                continue;
            }

            var termSet = taxonomyGroup.CreateTermSet(ts.Name);
            if (termSet == null)
            {
                continue;
            }

            this.createTerms(termSet, ts.Terms);
        }
    }

    private void createTerms(TermSet termSet, List<TermDTO> terms)
    {
        if (termSet == null)
        {
            return;
        }

        if (terms.IsNullOrEmpty())
        {
            return;
        }

        foreach (var term in terms)
        {
            if (termSet.Terms.Any(t => t.Name == term.Name))
            {
                continue;
            }
            var parentTerm = termSet.CreateTerm(term.Name, ENGLISH_LOCALE_ID);
            this.createTerms(parentTerm, term.Terms);
        }
    }

    private void createTerms(Term parentTerm, List<TermDTO> terms)
    {
        if (parentTerm == null)
        {
            return;
        }

        if (terms.IsNullOrEmpty())
        {
            return;
        }

        foreach (var term in terms)
        {
            if (parentTerm.Terms.Any(t => t.Name == term.Name))
            {
                continue;
            }
            var subTerm = parentTerm.CreateTerm(term.Name, ENGLISH_LOCALE_ID);
            this.createTerms(subTerm, term.Terms);
        }
    }
}

Код достаточно очевидный: во-первых он рекурсивно парсит xml файл и загружает результат в DTO объекты, определенные выше (см. методы load() и getTerms()). Затем он создает таксономии в банке терминов из объектной модели (см. методы createTermSets() и createTerms()) – также рекусрсивно. Для компиляции вам потребуется добавить ссылку на сборку Micosoft.SharePoint.Taxonomy.dll.

Также для компиляции нужно добавить метод-расширение IsNullOrEmpty() для типа IEnumerable<T>, описанный Филом Хааком в его блоге.

Код, приведенный выше, использует английскую локаль (lcid = 1033) для терминов. Вы можете расширить его и добавить поддержку переводов и синонимов.

После того, как вы установите wsp-пакет и активируете фичу в вашей сайт коллекции, будет создана иерархия таксономии в банке терминов:

Managed metadata

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

Реклама

Об авторе sadomovalex

Старший инженер, team lead, консультант. Работаю в стеке .Net. Последние несколько лет занимаюсь разработкой enterprise приложений под Sharepoint, чему и будет в основном посвящена тематика этого блога. Также активно использую и интересуюсь ASP.Net MVC, DDD, TDD, Agile. Активно участвую в жизни многих профессиональных сообществ, SPb .Net UG, SPb ALT.Net, rsdn, Finland SP UG и др.
Запись опубликована в рубрике Uncategorized. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s