Cross-site навигация в Sharepoint. Часть 3

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

Итак, у нас есть 2 сайта: http://example.com/site1 и http://example.com/site2, и мы хотим использовать навигацию сайта site1 на сайте site2. Для начала нам необходимо реализовать кастомный провайдер карты узлов навигации (custom site map provider) – наследник класса StaticSiteMapProvider, который будет вызывать веб метод нашего веб сервиса Navigation.asmx. Реализация провайдера показана ниже:

public class CustomNavigationProvider : StaticSiteMapProvider
{
    private const string SITE_MAP_SESSION_KEY = "CustomNavigationMap";

    private SPWeb getNavigationContextWebUrl()
    {
        // instead of hardcoding site url you can use your own logic here
        using (var web = SPContext.Current.Site.OpenWeb("/site1"))
        {
            return web.Url;
        }
    }

    private NavigationService initWebService(string contextWebUrl)
    {
        var proxy = new NavigationService();
        proxy.Url = SPUrlUtility.CombineUrl(contextWebUrl, "/_layouts/Custom/Navigation.asmx");
        // use another credentials if required instead of DefaultCredentials
        proxy.Credentials = CredentialCache.DefaultCredentials;
        return proxy;
    }

    public override void Initialize(string name, NameValueCollection attributes)
    {
        base.Initialize(name, attributes);
        // here you can add your initialization logic, e.g. initialize web service URL
    }
    
    public override SiteMapNode BuildSiteMap()
    {
        SiteMapNode node;
        if (HttpContext.Current.Session[SITE_MAP_SESSION_KEY] == null)
        {
            node = tryGetNavigationNodesFromContextWeb();
            HttpContext.Current.Session[SITE_MAP_SESSION_KEY] = node;
        }
        node = HttpContext.Current.Session[SITE_MAP_SESSION_KEY] as SiteMapNode;
        return node;
    }

    private SiteMapNode tryGetNavigationNodesFromContextWeb()
    {
        try
        {
            string webUrl = this.getNavigationContextWebUrl();
            var proxy = this.initWebService(webUrl);
            var doc = proxy.GetMenuItems();
            var collection = ConvertHelper.BuildNodesFromXml(this, doc);
            if (collection == null)
                return null;
            if (collection.Count == 0)
                return null;
            return collection[0];
        }
        catch(Exception x)
        {
            return new SiteMapNode(this, "/", "/", "");
        }
    }

    protected override SiteMapNode GetRootNodeCore()
    {
        SiteMapNode node = null;
        node = BuildSiteMap();
        return node;
    }
}

Я убрал некоторые методы, которые используются в реальном примере, но которые тем не менее не важны для данной статьи, и оставил лишь основные методы, раскрывающие суть идеи. Как и сказано в документации класса StaticSiteMapProvider, мы переопределили 3 метода: GetRootNodeCore(), Initialize() and BuildSiteMap(). Для того чтобы этот код скомпилировался, необходимо добавить ссылку на веб сервис Navigation.asmx для генерации прокси в Visual Studio (реализацию веб сервиса см. ниже). Т.к. мы не хотим вызывать веб метод при каждом обращении к сайту site2 (т.к. это сильно скажется на производительности – Sharepoint вызывает провайдер карты узлов навигации действительно часто при каждом запросе), я добавил простое кеширование, используя сессию (как вы знаете сессии в Sharepoint-е хранятся в SQL базе данных, поэтому такой подход будет работать в случае фермы с несколькими WFE). Как я и описывал в предыдущей части, провайдер вызывает метод веб сервиса на сайте site2 в контексте сайта site1.

Теперь рассмотрим детали реализации веб сервиса Navigation.asmx (его codebehind класс):

[WebService(Namespace = "http://example.com")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
public class NavigationService : WebService
{
    [WebMethod]
    public XmlDocument GetMenuItems()
    {
        PortalSiteMapDataSource ds = new PortalSiteMapDataSource();
        ds.SiteMapProvider = "CombinedNavSiteMapProvider";
        ds.EnableViewState = false;
        ds.StartFromCurrentNode = true;
        ds.StartingNodeOffset = 0;
        ds.ShowStartingNode = true;
        ds.TreatStartingNodeAsCurrent = true;
        ds.TrimNonCurrentTypes = NodeTypes.Heading;
        
        AspMenu m = new AspMenu();
        m.DataSource = ds;
        m.EnableViewState = false;
        m.Orientation = Orientation.Horizontal;
        m.StaticDisplayLevels = 2;
        m.MaximumDynamicDisplayLevels = 1;
        m.DynamicHorizontalOffset = 0;
        m.StaticPopOutImageTextFormatString = "";
        m.StaticSubMenuIndent = 0;
        m.DataBind();
        
        var doc = ConvertHelper.BuildXmlFromMenuItem(m.Items);
        return doc;
    }
}

Веб сервис содержит единственный веб метод GetMenuItems(), который вызывается в провайдере с использованием прокси (см. выше). Я использовал небольшой трюк: вместо использования PortalSiteMapProvider я использовал классы PortalSiteMapDataSource и AspMenu для того чтобы вернуть именно те узлы навигации, которые непосредственного отображаются на сайте site1 (я скопировал их настройки из master страницы, что не очень хорошо. Тем не менее, для примера этого достаточно). Есть разница между всеми узлами навигации для определенного сайта и теми узлами, которые на нем отображаются. Как я писал в другом своем посте The basics of navigation in Sharepoint отображение конфигурируется с помощью элементов управления site map data source и AspMenu (которые в большинстве случаев располагаются на master странице сайта). Конечно вы можете использовать непосредственно PortalSiteMapProvider и возвращать все узлы навигации.

Осталось описать вспомогательный класс ConverterHelper, который используется для двух целей:

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

Реализация класса ConverterHelper показана ниже:

public static class ConvertHelper
{
    public const string TAG_ROOT = "root";
    public const string TAG_NODE = "node";
    public const string ATTR_PATH = "path";
    public const string ATTR_URL = "url";
    public const string ATTR_TITLE = "title";

    public static SiteMapNodeCollection BuildNodesFromXml(SiteMapProvider provider, XmlNode doc)
    {
        try
        {
            var collection = new SiteMapNodeCollection();
            if (doc.ChildNodes.Count == 1 && doc.ChildNodes[0].Name == TAG_ROOT)
            {
                doc = doc.ChildNodes[0];
            }

            buildNodesFromXml(provider, doc, collection);
            return collection;
        }
        catch (Exception x)
        {
            return null;
        }
    }

    private static void buildNodesFromXml(SiteMapProvider provider, XmlNode parentNode, SiteMapNodeCollection collection)
    {
        foreach (XmlNode xmlNode in parentNode.ChildNodes)
        {
            if (xmlNode.Name == TAG_NODE)
            {
                var node = new SiteMapNode(provider, xmlNode.Attributes[ATTR_PATH].Value,
                                           xmlNode.Attributes[ATTR_URL].Value,
                                           xmlNode.Attributes[ATTR_TITLE].Value);

                if (xmlNode.HasChildNodes)
                {
                    var childNodes = new SiteMapNodeCollection();
                    buildNodesFromXml(provider, xmlNode, childNodes);
                    node.ChildNodes = childNodes;
                }

                collection.Add(node);
            }
        }
    }

    public static XmlDocument BuildXmlFromMenuItem(MenuItemCollection collection)
    {
        if (collection == null || collection.Count == 0)
        {
            return null;
        }

        var doc = new XmlDocument();

        var element = doc.CreateElement(TAG_ROOT);
        doc.AppendChild(element);

        foreach (MenuItem item in collection)
        {
            buildXmlFromMenuItem(item, doc, element);
        }

        return doc;
    }

    private static void buildXmlFromMenuItem(MenuItem item, XmlDocument doc, XmlNode xml)
    {
        if (item == null)
            return;

        XmlElement element = doc.CreateElement(TAG_NODE);
        element.SetAttribute(ATTR_PATH, item.DataPath);
        element.SetAttribute(ATTR_TITLE, item.Text);
        element.SetAttribute(ATTR_URL, item.NavigateUrl);

        xml.AppendChild(element);

        foreach (MenuItem childItem in item.ChildItems)
        {
            buildXmlFromMenuItem(childItem, doc, element);
        }
    }
}

Последнее действие, которое необходимо сделать – это сконфигурировать master страницу, чтобы использовать кастомный провайдер навигации. Я не буду повторяться о том, как это сделать – см. часть1.

В этой части я описал компоненты, которые необходимо реализовать для того чтобы использовать подход, основанный на вызовах веб сервиса. Одно из преимуществ этого подхода в том, что вы не ограничены пределами сайт коллекции или веб приложения (более того, вы не ограничены пределами одного веб сервера, т.к. можете вызывать веб сервис с другого сервера и показывать данные навигации с него – хотя в данный момент я не готов ответить, насколько эта возможность полезна ) ). Но с другой стороны с этим подходом нужно быть очень внимательным к вопросам производительности: нужно убедиться, что веб сервис не вызывается каждый раз при запросе к сайту (точнее к любой странице, которая использует master страницу с кастомным провайдером).

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

Реклама

Об авторе sadomovalex

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

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s