среда, 30 мая 2012 г.

Реализация паттерна UniqueURLs в SharePoint

На своем докладе на MsDevCon’12 я рассказывал о нескольких паттернах, и в частности — о паттерне Unique URLs. Также, я показывал, как этот паттерн работает на реальном примере – созданном мной Ajax-дашбоарде. Этот паттерн крайне актуален для Ajax-приложений, и строится вокруг следующего утверждения:
У каждого состояния страницы должен быть уникальный адрес
Это означает, что когда вы нажимаете на некой странице на кнопку или ссылку, и состояние (внешний вид) страницы меняется (неважно, был ли это postback или callback) — то у этой страницы должен измениться и адрес (т.е. текст в строке браузера):

image
Если каждый раз адрес менять не получается (например, в случае карт), то нужно предусмотреть кнопку «Получить ссылку» или что-то в этом духе.

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

image
Надеюсь, задача понятна, а теперь давайте рассмотрим реализацию, и некоторые тонкости при реализации этого паттерна в SharePoint.

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

Я стараюсь при возможности ничего не подключать дополнительного и писать как можно меньше кастомного кода (потому что его потом еще поддерживать), в общем стараюсь по максимуму использовать то, что уже есть в SharePoint.

В частности, в SharePoint «из коробки» уже подключена библиотека ASP.Net Ajax версии 3.5, и оказывается, в этой библиотеке есть встроенные средства для реализации якорной навигации, что нам вполне подходит. На MSDN есть статья Managing Browser History Using Client Script, которая объясняет, как работать с якорной навигацией с помощью ASP.Net Ajax.

Если вкратце, потребуется всего два метода:
  1. При изменении состояния страницы необходимо менять текущий адрес и добавлять соответствующий переход в историю браузера. Это делает метод Sys.Application.addHistoryPoint.
  2. При изначальном заходе на страницу, нам нужно проанализировать текущий адрес и восстановить сохраненное в анкоре состояние страницы. Для этого необходимо подписаться на событие Sys.Application.navigate.
Но при корректном использовании этой связки двух методов, в SharePoint’е всплывает небольшая проблема... А именно, перестает работать переключение локализации «на лету». Возможно, также перестают работать и какие-то другие функции – точно не знаю. Немного покопавшись в отладчике, я выяснил, что проблема зарылась в функции OnSelectionChange:

function OnSelectionChange(value) {
    var today = new Date();
    var oneYear = new Date(today.getTime() + 365 * 24 * 60 * 60 * 1000);
    var url = window.location.href;
    document.cookie = "lcid=" + value + ";path=/;expires=" + oneYear.toGMTString();
    window.location.href = url;
}

Здесь присваивание window.location.href = url; должно по идее вызывать перезагрузку страницы, но т.к. на конце url при использовании анкорной навигации идет символ решетка (“#”) и параметры, идентифицирующие текущее состояние страницы (например, “http://site/page.aspx#state=10”), то перезагрузки страницы не происходит, т.к. браузер думает, что мы хотим перейти на анкор внутри текущей страницы.

Довольно забавный баг, но неприятный. Вышеозначенная функция “OnSelectionChange” генерируется прямо в код страницы, непосредственно перед кодом риббона. Редиску, которая генерирует эту функцию, я вычислять не стал. Вместо этого, просто исправил в ней ошибку и теперь переопределяю её после начальной загрузки страницы.

В конечном итоге, класс, ответственный за реализацию Unique URLs в моем проекте, выглядит следующим образом:

/// <reference path="GridController.js" />
DW.TasksDashboard._UniqueURLs = function () {

    DW.TasksDashboard.DataLoadManager.add_dataLoaded(onDataLoaded);

    this.FixOnFlyLocalization = function () {
        window.OnSelectionChange = function (value) {
            var today = new Date();
            var oneYear = new Date(today.getTime() + 365 * 24 * 60 * 60 * 1000);
            var url = window.location.href.replace(/#[^#]*$/, '');
            document.cookie = 'lcid=' + value + ';path=/;expires=' + oneYear.toGMTString();
            window.location.href = url;
        }
    }

    Sys.Application.add_navigate(function (sender, e) {
        var state = e.get_state();

        // Валидируем значения, т.к. они получены из Query String и могут быть потенциально небезопасны
        var view = parseInt(state.view);
        var filter = parseInt(state.filter);
        var prefs = parseInt(state.prefs);

        if (isNaN(view) || isNaN(filter) || isNaN(prefs))
            return;

        DW.TasksDashboard.ViewManager.RestorePreviouslySavedView(state);
    });

    function onDataLoaded() {
        Sys.Application.addHistoryPoint({
            view: DW.TasksDashboard.ViewManager.get_CurrentViewFlags(),
            filter: DW.TasksDashboard.ViewManager.get_CurrentFilterFlags(),
            prefs: DW.TasksDashboard.ViewManager.get_CurrentAdditionalFlags()
        });
    }

}

DW.TasksDashboard.UniqueURLs = new DW.TasksDashboard._UniqueURLs();
_spBodyOnLoadFunctionNames.push("DW.TasksDashboard.UniqueURLs.FixOnFlyLocalization");

Он зависит от некоторых других классов моего проекта, но я надеюсь, он очень хорошо демонстрирует концепцию паттерна UniqueURLs, и заодно, этот пример должен вам помочь правильно изолировать логику UniqueURLs от всего остального решения. Мне кажется, здесь эта логика изолирована очень удачно.
В итоге, адресная строка моего дашбоарда выглядит примерно так:


image

, т.е. параметры в строке состояния полностью соответствуют положению переключателей в трех разных группах на контекстной ленте (Views, Filters и Preferences). И если мы начинаем переключать кнопки на ленте, эти переключения тут же отражаются в адресной строке.

Мне кажется, это очень классно и очень удобно. Решается как минимум одна бизнес-задача: например, если клиент вдруг захочет, чтобы для всех пользователей дашбоард открывался по умолчанию на фильтре “Задачи, назначенные на меня моим руководителем”, или на какой-то другой особой комбинации настроек/фильтров/представлений, то он сможет просто отредактировать ссылку в главном меню – и вуаля, проблема решена.

Подводя итог: очень рекомендую обратить внимание на Ajax-паттерны и использовать их, если они подходят вашему решению – часто эти паттерны требуют совершенно пустяковых затрат в реализации, но зато могут значительно повысить удобство ваших интерфейсов, особенно если вы реализуете не один, а хотя бы 2-3 паттерна. Список Ajax-паттернов можно найти на сайте AjaxPatterns.org.

2 комментария:

  1. А можно немного подробнее объяснить этот момент - "Немного покопавшись в отладчике, я выяснил, что проблема зарылась в функции OnSelectionChange". Имеется ввиду обычный браузерный Firebug (и аналоги) или что-то посерьезнее?

    ОтветитьУдалить

Внимание! Реклама и прочий спам будут беспощадно удаляться.

Примечание. Отправлять комментарии могут только участники этого блога.