Создаем асинхронные веб-части в Sharepoint

Один из способов улучшить производительность вашего Sharepoint сайта – добавить поддержку ajax в ваши кастомные веб-части (мне больше нравиться название веб-парта, но в переводах чаще используется термин веб-часть, поэтому я тоже буду использовать его в статье). Как вы наверное знаете, в Sharepoint есть несколько стандартных веб-частей, которые поддерживают ajax “из коробки”. Например, в свойствах веб-части Search core results есть категория AJAX Options, в которой можно установить настройки асинхронной загрузки (нет русской версии Sharepoint-а, поэтому скриншот английский):

image

Если выбрать опцию “Enable Asynchronous Load”, веб-часть будет загружаться асинхронно и не будет блокировать загрузку страницы. Однако такая возможность есть не у всех стандартных веб-частей. Кастомные веб-части по умолчанию также загружаются синхронно, и, чтобы добавить поддержку ajax в них, необходимо совершить дополнительные действия.

В данной статье я покажу как создавать асинхронные веб-части с использованием фреймворка для асинхронной загрузки веб-частей Sharepoint с codeplex. Конечное, это не единственное возможное решение, но я решил использовать его, т.к. оно неплохо расширяется и может быть использовано для многих веб-частей в вашем проекте. Основная идея показана на следующем рисунке:

image

Начнем с класса веб-части. Он должен наследовать класс BaseWebPart из проекта фреймворка (в свою очередь BaseWebPart наследует WebPart – стандартный базовый класс веб-частей ASP.Net). BaseWebPart содержит два метода: один для создания контейнера для отображения индикатора загрузки, другой – для регистрации прокси веб-сервиса для его использования в javascript. Упрощенная версия выглядит следующим образом:

public class BaseWebPart : WebPart
{
    protected Panel contentContainer;

    protected void PrepareContentContainer()
    {
        try
        {
            if (HttpContext.Current.Request.Browser.EcmaScriptVersion.Major >= 1)
            {
                //Busy Box View (JS enabled)
                Image spinnerImage = new Image();
                spinnerImage.ImageUrl = String.Format("{0}{1}",
SPContext.Current.Web.Url, "/_layouts/AsyncWebpart/Images/loadingspinner.gif");
                Label spinnerText = new Label()
{ Text = "Please wait while the results are being loaded.." };

                contentContainer.ID = "" + this.ID + "Context";
                contentContainer.Controls.Add(spinnerImage);
                contentContainer.Controls.Add(new LiteralControl("<br />"));
                contentContainer.Controls.Add(spinnerText);
                //AJAX Enabled
            }
            else
            {
                //AJAX Disabled - Fallback mode
                 //Alternative content in case the user's browser does not support JS
                contentContainer.Controls.Add(
new LiteralControl("It seems your browser doesn't support Javascript"));
            }
        }
        catch (Exception ex)
        {
            // log
        }
    }

    protected void AddScriptManagerReferenceProxy(string referenceProxyUrl,
string referenceProxyJS)
    {
        try
        {
            ScriptManager thisScriptManager = ScriptManager.GetCurrent(this.Page);
            if (thisScriptManager == null)
            {
                thisScriptManager = new ScriptManager();
                this.Controls.Add(thisScriptManager);
            }

            ServiceReference referenceProxy = new ServiceReference();
            referenceProxy.Path = referenceProxyUrl;

            referenceProxy.InlineScript = true;
            //If you are having issues identifying the names of the web service JS methods, uncomment the line above ^

            thisScriptManager.Services.Add(referenceProxy);

            //JS file contains proxy methods for web service - see file for details

            this.Page.ClientScript.RegisterClientScriptInclude(typeof(Page), referenceProxyUrl, referenceProxyJS);
        }
        catch (Exception ex)
        {
            // log
        }
    }
}

Веб-сервис будет описан ниже. В данный момент важно понимать, что мы регистрируем прокси веб-сервиса, который можно использовать на клиенте.

Для тестирования создадим простую веб-часть с единственным кастомным свойством Delay. В нем будем хранить время задержки в секундах, которое будет ждать веб-сервис прежде, чем вернуть строку с разметкой элемента управления клиенту. Пример достаточно искусственный, но таким образом мы сможем сымитировать долгую операцию на стороне сервера и посмотреть, как веб-часть будет работать при этом. Код веб-части выглядит следующим образом:

public class AsyncCustomWebPart : BaseWebPart
{
    [WebBrowsable(true)]
    [Personalizable(PersonalizationScope.Shared)]
    public int Delay { get; set; }

    protected override void OnLoad(EventArgs e)
    {
        try
        {
            EnsureChildControls();

            // register script to automatically load the web part into the content container
            // pass all the property values through asynchronous call
            this.Page.ClientScript.RegisterStartupScript(typeof (Page),
                "startupScript" + this.ClientID,
                string.Format("AsyncWebPartGetData('{0}', '{1}');",
                    this.Delay, contentContainer.ClientID), true);

            base.OnLoad(e);
        }
        catch (Exception ex)
        {
            // handle error
        }
    }

    protected override void CreateChildControls()
    {
        try
        {
            contentContainer = new Panel();

            // prepare container to hold the content
            PrepareContentContainer();
            // add web service reference and js handler
            AddScriptManagerReferenceProxy(
                "/_layouts/AsyncWebPart/AsyncWebPartService.asmx",
                "/_layouts/AsyncWebPart/JS/AsyncWebPart.js");
        }
        catch (Exception ex)
        {
            // handle error
        }
    }

    public override void RenderControl(HtmlTextWriter writer)
    {
        contentContainer.RenderControl(writer);
        base.RenderControl(writer);
    }
}

Здесь в методе OnLoad мы вызываем EnsureChildControls для того, чтобы быть уверенными, что дочерние элементы управления созданы (строка 11). В методе CreateChildControls (строки 28-45) мы создаем элемент управления Panel, который будет играть роль контейнера, в который будет загружаться разметка, полученная из веб-сервиса. Как мы уже видели в методе PrepareContentContainer (строка 35) мы загружаем анимированный gif, отображающий индикатор загрузки веб-части до тех пор, пока ее содержимое не будет готово:

image

После этого мы добавляем прокси для асинхронного вызова веб-сервиса и ссылку на файл AsyncWebPart.js, который является частью фреймворка (строки). Код из этого файла будет показан ниже.

Затем в методе OnLoad мы регистрируем javascript код, который должен запускаться при загрузке страницы, для вызова нашей функции AsyncWebPartGetData. Эта функция также определена в AsyncWebPart.js. При вызове AsyncWebPartGetData мы передаем все кастомные свойства нашей веб-части. В свою очередь эти свойства далее передаются в веб-метод. Таким образом в асинхронной версии веб-части мы имеем возможность менять ее поведение, изменяя свойства, так же, как в синхронной версии.

Что происходит после этого? При загрузке страницы вызывается наша javascript функция. Посмотрим код AsyncWebPart.js:

function AsyncWebPartGetData(delay, containerID) 
{
    //Remember to use entire namespace + class before method name
    AsyncWebPart.AsyncWebPartService.GetUIHtmlForAsyncWebPart(
        delay, AsyncWebPartComplete, AsyncWebPartError, containerID);
}

//Client-side onComplete handler
function AsyncWebPartComplete(response, containerID) 
{
    //Populates the container with the response
    var containerDiv = document.getElementById(containerID);
    containerDiv.innerHTML = response;
}

//Client-side onError handler
function AsyncWebPartError(response, containerID) 
{
    var containerDiv = document.getElementById(containerID);
    containerDiv.innerHTML = "An error has occurred. Please contact administrator.";
}

Функция AsyncWebPartGetData используется только для нашей конкретной веб-части (более точно, для типа этой веб-части). Т.е. если вам нужно сделать другую веб-часть асинхронной, то будет нужно создать новую функцию. Две другие функции (AsyncWebPartComplete и AsyncWebPartError) могут использоваться для многих веб-частей. Как я уже говорил выше, AsyncWebPartGetData получает все кастомные свойства веб-части в параметрах (конечно, можно передавать и стандартные свойства, если это необходимо). В нашем примере это delay и containerID (containerID нужно передавать последним параметром во всех других функциях, которые вы создадите для ваших веб-частей. Он используется в функциях обратного вызова для загрузки html разметки из веб-сервиса). Внутри функции AsyncWebPartGetData асинхронно вызывается сгенерированный прокси, которому передаются delay, callbacks и containerID (строки 4,5). После этого управление возвращается странице.

В реальности регистрация javascrtipt функции и асинхронный вызов веб-сервиса с клиента выполняются очень быстро, так что веб-части практически не влияет на время загрузки страницы. С синхронным вариантом, страница не отобразится пользователю, пока контент веб-части не будет полностью готов. Так что если у вас есть веб-части, которые работают медленно, вы можете использовать эту технику для улучшения производительности и отзывчивости приложения. Но давайте продолжим.

Когда запрос достигает веб-сервиса, он делает следующее:

[System.Web.Script.Services.ScriptService]
[WebService(Namespace = "http://tempuri.org/", Name = "AsyncWebPartService")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
public class AsyncWebPartService : System.Web.Services.WebService
{
    [ScriptMethod]
    [WebMethod]
    public string GetUIHtmlForAsyncWebPart(int delay)
    {
        var ctrl = new MyControl();
        ctrl.Delay = delay;

        var retOutputBuilder = new StringBuilder();
        using (var stringWriter = new StringWriter(retOutputBuilder))
        {
            using (var textWriter = new HtmlTextWriter(stringWriter))
            {
                ctrl.RenderControl(textWriter);
            }
        }
        return retOutputBuilder.ToString();
    }
}

Сначала создается экземпляр элемента управления (строка 11). Затем ему передаются свойства, полученные из веб-части (строка 12). В этом примере мы передаем свойство delay, и элемент управления не делает ничего кроме того, что вызывает Thread.Sleep(). В реальных приложениях вы можете таким образом передавать все необходимые свойства веб-части. Затем веб-сервис рендерит элемент управления в строку (строки 14-22) и возвращает результат.

Обратите внимание, что т.к. в веб-методе GetUIHtmlForAsyncWebPart мы фактически получаем разметку для нашей веб-части, такие методы нужно будет создавать для каждой веб-части, которую необходимо сделать асинхронной. Затем, сгенерировать прокси для использования на клиенте и создать javascript функцию в AsyncWebPart.js.

Еще один момент, который стоит упомянуть. В нашем примере мы использовали кастомный элемент управления (custom control), но также возможно использовать пользовательские элементы управления (user controls) – см. например ASP.NET how to Render a control to HTML.

После этого html возвращается вызывающей стороне и обработчик javascript загружает его в контейнер внутри веб-части. Это происходит асинхронно без перезагрузки страницы.

Последнее, что я хотел бы отметить, это то, что если вы захотите использовать описанный подход, нужно будет разделить код веб-части от непосредственной логики UI. Т.е. веб-часть будет лишь легковесным контейнером. Но это скорее хороший стиль программирования, чем ограничение (например стандартные visual веб-части в Sharepoint реализованы таким образом: веб-часть – это контейнер, который загружает пользовательский элемент управления динамически).

Это все, что я хотел бы рассказать о фреймворке для создания асинхронных веб-частей. Отдельное спасибо автору проекта Chaitu Madala, который поделился подходом с сообществом. Надеюсь, эта статья поможет вам, если вы решите использовать фреймворк в своих проектах.

Реклама

Об авторе sadomovalex

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

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

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

Логотип WordPress.com

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

Фотография Twitter

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

Фотография Facebook

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

Google+ photo

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

Connecting to %s