Слайды с SPCUA и DevCon

За последние две недели я успел выступить с двумя докладами на конференция SharePoint Conferencе Ukraine 2013  в Киеве и DevCon 2013 в Москве.

Первый доклад посвящен вопросам аутентификации и авторизации в приложениях SharePoint 2013 (apps).

Второй доклад, который я делал совместно с Маратом Бакировым, о том как создавать приложения в SharePoint 2013.

Видеозаписи выступлений будут доступны позже.



Разработка приложений SharePoint 2013 с помощью TypeScript

Прошлый раз я описывал преимущества использования TypeScript для разработки приложений.

В этом посте я покажу как TypeScript поможет разрабатывать приложения для SharePoint 2013. В SharePoint 2013 были улучшены возможности разработки клиентских приложений на JavaScript. Это касается не только API, доступных на клиенте, но и механизмов поставки и развертывания приложений, инструментов разработчика. Более того, многие функции самого SharePoint 2013 реализованы и могу быть кастомизированы с помощью JavaScript.

SharePoint 2013 предлагает два вида API для использования на клиентской стороне: Client-Side Object Model (CSOM) и REST API. REST API позволяет манипулировать объектами на сервере используя REST (OData) веб-сервис. CSOM представляет из себя набор классов, семантически эквивалентных серверной объектной модели SharePoint. CSOM доступна как для JavaScript (также называют JSOM – JavaScript Object Model) , так и для .NET. Но в JavaScript, в отличие от .NET, недоступны метаданные и типизация. В этой статье будет рассмотрено именно применение JSOM.

TypeScript позволяет описать типы для JSOM и использовать статическую проверку типов и intellisense при разработке приложений. К сожалению готовых определений типов для SharePoint 2013 в открытом доступе нет.

Я и Андрей Маркеев создали проект на CodePlex, в котором сделали определения типов и кучу примеров приложений на TypeScript для SharePoint 2013. Ссылка на проект - http://sptypescript.codeplex.com/

Пример приложения

Для примера создам приложение, позволяющее отслеживать время на рабочем месте.

image

Подготовка

Для начала необходимо:

Для того чтобы при сборке проекта выполнялась компиляция  TypeScript необходимо добавить в .csproj файл следующие элементы:

<PropertyGroup>
    <TypeScriptTarget>ES3</TypeScriptTarget>
    <TypeScriptIncludeComments>true</TypeScriptIncludeComments>
    <TypeScriptSourceMap>true</TypeScriptSourceMap>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" />

 

Библиотеки и определения

Визуальный интерфейс будет создан с помощью библиотеки knockoutjs с расширением koLite.

Для того чтобы использовать эти библиотеки в проекте необходимо добавить следующие NuGet пакеты:

  • KoLite (knockoutjs добавится автоматически)
  • jquery.TypeScript.DefinitelyTyped
  • knockout.TypeScript.DefinitelyTyped
  • kolite.TypeScript.DefinitelyTyped

Последние три пакета представляют из себя .d.ts файлы, которые описывают типы для TypeScript.

Для работы с JSOM в TypeScript надо добавить в проект файл SharePoint.d.ts, который можно найти по ссылке. NuGet пакет будет доступен в ближайшее время.

Загрузка скриптов по требованию

В SharePoint есть свой загрузчик скриптов по требованию в классе SP.SOD. Подробное описание можно найти в этом посте.

Код загрузчика скриптов приложения:

///<reference path="typings/SharePoint.d.ts" />
///<reference path="typings/jquery/jquery.d.ts" />
///<reference path="typings/knockout/knockout.d.ts" />

/// <reference path="ViewModel.ts" />


$(() => {
    SP.SOD.registerSod('ViewModels', _spPageContextInfo.webServerRelativeUrl + '/Scripts/ViewModel.js');
    SP.SOD.registerSodDep('ViewModels', 'sp.js');

    SP.SOD.executeFunc('ViewModels', null, () => {
        var vm = new ViewModels.Model(SP.ClientContext.get_current());
        ko.applyBindings(vm);
    });
});


Модель представления

Разметка страницы приложения:

<div>
    <p data-bind="text:message"></p>
    <button data-bind="text:buttonText, command: checkInOut, visible:isLoaded" style="display:none;"/>
</div>

Используется плагин koLite для асинхронных команд.

Код модели представления:

module ViewModels {
    export class Model {
        constructor(public context: SP.ClientContext) {
            this.isLoaded = ko.observable(false);
            this.message = ko.observable('');
            this.buttonText = ko.observable('');

            this.checkInOut = ko.asyncCommand({
                canExecute: (isExecuting) => !isExecuting && this.isLoaded(),
                execute: this.executeCheckInOut
            });

            this.init();
        }

        public message: KnockoutObservableString;
        public buttonText: KnockoutObservableString;
        public checkInOut: KoliteCommand;
        public isLoaded: KnockoutObservableBool;

        //...
    }
}

Все типы описаны в .d.ts файлах и проверяются при компиляции.

Инициализация модели

JSOM при выполнении формирует очередь команд, отправляемых на сервер функцией SP.ClientContext.executeQueryAsync. executeQueryAsync принимает два коллбека, первый вызывается в случае успешного завершения, второй в случае неудачи. Внимание, указатель this портится внутри коллбеков функции executeQueryAsync, но если указывать коллбеки в виде лямбд, то TS заботливо генерирует код, который сохраняет указатель this.

private init() {
    this.list = this.context.get_web().get_lists().getByTitle('Log');
    var items = this.list.getItems(SP.CamlQuery.createAllItemsQuery());
    this.context.load(items);

    this.context.executeQueryAsync(
        () => {
            this.processItems(items);
            this.setData();
            this.isLoaded(true);
        },
        (sender, args) => alert(args.get_message()));
};

Запрос множества элементов в JSOM возвращает не массив, а коллекцию объектов, реализующую интерфейс IEnumerable,  хотя внутри объекта лежит массив. Это все вызвано тем, что большая часть клиентской объектной модели сгенерирована из серверной объектной модели, и все коллекции требуют специальный паттерн для обхода. Он 100% соответствует коду .NET для обработки IEnumerable коллекций.

Обработка результатов запроса:

private processItems(items: SP.ListItemCollection) {
    this.hoursSubmitted = 0;
    var enumerator = items.getEnumerator();
    while (enumerator.moveNext()) {
        var item = <SP.ListItem>enumerator.get_current();
        var author = <SP.FieldUserValue>item.get_item('Author');
        //Filter by current user
        if (author.get_lookupId() == _spPageContextInfo.userId) {
            var dateCompleted = item.get_item('DateCompleted');
            if (dateCompleted) {
                this.hoursSubmitted += item.get_item('DurationInHours');
            } else {
                this.curentItem = item;
            }
        }
    }
}

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

Обработка команд

В зависимости от текущего состояния модели выполняется Check-In или Check-Out

private executeCheckInOut(complete: () => void ) {
    if (this.curentItem) {
        this.checkOut(complete);
    } else {
        this.checkIn(complete);
    }
};

Операция Check-In заключается в создании нового элемента в списке SharePoint, без указания времени завершения.

private checkIn(complete: () => void ) {
    var item = this.list.addItem(new SP.ListItemCreationInformation());
    item.set_item('StartDate', new Date());
    item.update();

    this.context.executeQueryAsync(
        () => {
            this.curentItem = item;
            this.setData();
            complete();
        },
        (sender, args) => {
            alert(args.get_message());
            complete();
        });
}

 

Противоположная операция – Check-Out – заполняет значения времени завершения и продолжительности в часах.

private checkOut(complete: () => void ) {
    var startedDate = <Date>this.curentItem.get_item('StartDate');
    var dateCompleted = new Date();
    var hours = (dateCompleted.getTime() - startedDate.getTime()) / (1000 * 60 * 60);

    this.curentItem.set_item('DateCompleted', dateCompleted);
    this.curentItem.set_item('DurationInHours', hours);
    this.curentItem.update();

    this.context.executeQueryAsync(
        () => {
            this.curentItem = null;
            this.hoursSubmitted += hours;
            this.setData();
            complete();
        },
        (sender, args) => {
            alert(args.get_message());
            complete();
        });
}

В обоих случаях используется один и тот же “паттерн”. Сначала формируется пакет команд для отправки на сервер, а после успешного применения изменения отражаются в модели.

Заключение

Полный код примера вы можете скачать по ссылке. Также рекомендую посмотреть код проекта и примеры использования определений TypeScript для SharePoint (source code), найдете много интересного.

Кстати сам код примера будет работать и в SharePoint 2010, но придется создать другой проект и по-другому развертывать артефакты решения, чтобы все вместе заработало.

А в следующий раз я расскажу как можно кастомизировать формы и представления списков в SharePoint 2013, и тоже с помощью TypeScript.



Почему вам стоит использовать TypeScript

Если вы еще не в курсе: JavaScript победил. На сегодняшний день это самый кроссплатформенный язык, доступный для любых устройств. На нем можно создавать веб-приложения (клиент и сервер), в том числе с оффлайн-режимом работы, десктопные приложения (для Windows 8), приложения для смартфонов и планшетов (PhoneGap), расширения для Microsoft Office, SharePoint и Dynamics. Код на JavaScript работает в СУБД, таких как MongoDB и даже Hadoop в Windows Azure (BigData однако).

На Javascript уже написаны Doom и эмулятор Linux. Фактически решая любую задачу, кроме написания модуля ядра ОС, вы встретитесь с JavaScript. Если вы еще не знаете JavaScript, то вам следует срочно начать его изучать.

У JavaScript  много недостатков

Многие разработчики испытывают такую реакцию на JavaScript:

image

(картинка взята отсюда)

Большую часть людей пугает отсутствие intellisense при наборе кода и манипуляции с HTML\CSS. Но это не самые большие проблемы.

Самая большая проблема JavaScript в том, что его придумал не Microsoft.

Основные недостатки JavaScript:

  • Динамическая типизация, которая вызывает множество регрессионных ошибок.
  • Отсутствие модульности. Нет ни модулей, ни классов, прототипное ООП рвет мозг тем, кто пишет на C++\Java\C#.
  • Неочевидное поведение во многих местах.

Для того чтобы не писать JavaScript были созданы компиляторы Java –> JavaScript, C# –> JavaScript, LLVM –> JavaScript. Но все это приводит к тому, что на программу на исходном языке накладываются существенные ограничения, а также существующие библиотеки для JavaScript не используются.

TypeScript исправляет часть недостатков

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

Так и появился язык TypeScript (ссылка на оффсайт). TypeScript является надмножеством JavaScript. То есть любой корректный код на JavaScript также является корректным кодом на TypeScript.

TypeScript использует статическую типизацию, то есть все типы проверяются при компиляции. Кстати сам компилятор TypeScript написан на TypeScript и является open source (ссылка на репозитарий).

TypeScript добавляет возможность объявлять модули, классы и интерфейсы. Это позволяет масштабировать разработку сложных JavaScript приложений.

На выходе получается обычный JavaScript, который не требует дополнительных библиотек или специальных компонентов.

TypeScript в действии

При наборе кода в VisualStudio доступна богатая подсказка:

tsintellisence

Поддерживаются классы, аннотации и вывод типов, работает интерактивная отладка.

С легкостью можно использовать в TypeScript внешние библиотеки, например jquery:

image

При несовпадении типов компилятор ругается:

image

Компиляция TypeScript  происходит при сборке проекта, проверяя многие ошибки без запуска.

Ну и самая главная фича, от вида которой я чуть не расплакался:

image

Кто еще не до конца вдохновился может глянуть на raytracer на TypeScript, результат работы.

Как начать использовать TypeScript

Если вы используете VisualStudio, то вам необходимо поставить два расширения:

Тогда у вас появится вот такой режим редактирования:

Слева код на TypeScript, справа результат компиляции на JavaScript. Таким образом использование TypeScript поможет вам лучше понять и изучить JavaScript.

Тем, кто не использует visual studio, повезло чуть меньше. На сегодня доступны плагины для распространенных текстовых редакторов, а также инструменты командной строки.

Далее вам следует обязательно прочитать туториалы от настоящего папы, иногда стоит заглядывать в спецификацию.

Множество определений типов для популярных библиотек можно найти в проекте DefinitelyTyped. Судя по нику автора проекта это наш соотечественник.

Заключение

Это была вводная статья. В следующий раз больше примеров и реальное применение TypeScript.



Запись с доклада на SPC UA 2012

Прошел почти год с того момента, как я выступал на конференции в Киеве. Наконец-то организаторы выложили запись выступления.

Смотреть тут: http://sharepoint-channel.com/stanislav-vyshhepan-iskusstvo-upravleniya-sharepoint-kak-poluchit-maksimalnuyu-vygodu-dlya-biznesa-videozapis-doklada-na-spcua-2012



Кеширование в ASP.NET MVC

В прошлом посте я рассказывал о различных стратегиях кеширования. Там была голая теория, которая и так всем известна, а кому неизвестна, тому без примеров ничего не понятно.

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

Для примера я взял приложение MVC Music Store, которое используется в разделе обучение на сайте asp.net. Приложение представляет из себя интернет-магазин, с корзиной, каталогом товаров и небольшой админкой.

Исследуем проблему

Сразу создал нагрузочный тест на одну минуту, который открывает главную страницу. Получилось 60 страниц в секунду (все тесты запускал в дебаге). Это очень мало, полез разбираться в чем проблема.

Код контроллера главной страницы:
public ActionResult Index()
{
    // Get most popular albums
    var albums = GetTopSellingAlbums(5);
    return View(albums);
}

private List<Album> GetTopSellingAlbums(int count)
{
    // Group the order details by album and return
    // the albums with the highest count

    return storeDB.Albums
        .OrderByDescending(a => a.OrderDetails.Count())
        .Take(count)
        .ToList();
}


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

При этом в каждой странице выводится персонализированная информация — количество элементов в корзине.
Код _layout.cshtml (Razor):
<div id="header">
    <h1><a href="/">ASP.NET MVC MUSIC STORE</a></h1>
    <ul id="navlist">
        <li class="first"><a href="@Url.Content("~")" id="current">Home</a></li>
        <li><a href="@Url.Content("~/Store/")">Store</a></li>
        <li>@{Html.RenderAction("CartSummary", "ShoppingCart");}</li>
        <li><a href="@Url.Content("~/StoreManager/")">Admin</a></li>
    </ul>        
</div>


Такой «паттерн» часто встречается в веб-приложениях. На главной странице, которая открывается чаще всего, выводится в одном месте статистическая информация, которая требует больших затрат на вычисление и меняется нечасто, а в другом месте — персонализированная информация, которая часто меняется. Из-за этого главная страница работает медленно, и средствами HTTP её кешировать нельзя.

Делаем приложение пригодным для кеширования

Чтобы такой ситуации, как описано выше, не происходило надо разделить запросы и собирать части страницы на клиенте. В ASP.NET MVC это сделать довольно просто.
Код _layout.cshtml (Razor):
<div id="header">
    <h1><a href="/">ASP.NET MVC MUSIC STORE</a></h1>
    <ul id="navlist">
        <li class="first"><a href="@Url.Content("~")" id="current">Home</a></li>
        <li><a href="@Url.Content("~/Store/")">Store</a></li>
        <li><span id="shopping-cart"></span></li>
        <li><a href="@Url.Content("~/StoreManager/")">Admin</a></li>
    </ul>        
</div>

<!-- skipped -->

<script>        
    $('#shopping-cart').load('@Url.Action("CartSummary", "ShoppingCart")');
</script>


В коде контроллера:
//[ChildActionOnly] //Убрал
[HttpGet] //Добавил
public ActionResult CartSummary()
{
    var cart = ShoppingCart.GetCart(this.HttpContext);

    ViewData["CartCount"] = cart.GetCount();
    this.Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache); // Добавил
    return PartialView("CartSummary");
}


Установка режима кеширования NoCache необходима, так как браузеры могут по умолчанию кешировать Ajax запросы.

Само по себе такое преобразование делает приложение только медленнее. По результатам теста — 52 страницы в секунду, с учетом ajax запроса для получения состояния корзины.

Разгоняем приложение


Теперь можно прикрутить lazy кеширование. Саму главную страницу можно кешировать везде и довольно долго (статистика терпит погрешности).
Для этого можно просто навесить атрибут OutputCache на метод контроллера:
[OutputCache(Location=System.Web.UI.OutputCacheLocation.Any, Duration=60)]
public ActionResult Index()
{
    // skipped
}


Чтобы оно успешно работало при сжатии динамического контента необходимо в web.config добавить параметр:
<system.webServer>
  <urlCompression dynamicCompressionBeforeCache="false"/>
</system.webServer>

Это необходимо чтобы сервер не отдавал заголовок Vary:*, который фактически отключает кеширование.

Нагрузочное тестирование показало результат 197 страниц в секунду. Фактически страница home\index всегда отдавалась из кеша пользователя или сервера, то есть настолько быстро, насколько возможно и тест померил быстродействие ajax запроса, получающего количество элементов в корзине.

Чтобы ускорить работу корзины надо сделать немного больше работы. Для начала результат cart.GetCount() можно сохранить в кеше asp.net, и сбрасывать кеш при изменении количества элементов в корзине. Получится в некотором роде write-through кеш.

В MVC Music Store сделать такое кеширование очень просто, как так всего 3 экшена изменяют состояние корзины. Но в сложном случае, скорее всего, потребуется реализации publish\subscribe механизма в приложении, чтобы централизованно управлять сбросом кеша.

Метод получения количества элементов:
[HttpGet]
public ActionResult CartSummary()
{
    var cart = ShoppingCart.GetCart(this.HttpContext);
    var cacheKey = "shooting-cart-" + cart.ShoppingCartId;

    this.HttpContext.Cache[cacheKey] = this.HttpContext.Cache[cacheKey] ?? cart.GetCount();

    ViewData["CartCount"] = this.HttpContext.Cache[cacheKey];

    return PartialView("CartSummary");
}


В методы, изменяющие корзину, надо добавить две строчки:
var cacheKey = "shooting-cart-" + cart.ShoppingCartId;
this.HttpContext.Cache.Remove(cacheKey);


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

Используем HTTP кеширование

Последний аккорд — прикручивание HTTP кеширование к запросу количества элементов в корзине. Для этого нужно:
  1. Отдавать Last-Modified в заголовках ответа
  2. Обрабатывать If-Modified-Since в заголовках запроса (Conditional GET)
  3. Отдавать код 304 если значение не изменилось


Начнем с конца.
Код ActionResult для ответа Not Modified:
public class NotModifiedResult: ActionResult
{
    public override void ExecuteResult(ControllerContext context)
    {
        var response = context.HttpContext.Response;
        response.StatusCode = 304;
        response.StatusDescription = "Not Modified";
        response.SuppressContent = true;
    }
}


Добавляем обработку Conditional GET и установку Last-Modified:
[HttpGet]
public ActionResult CartSummary()
{
    //Кеширование только на клиенте, обновление при каждом запросе
    this.Response.Cache.SetCacheability(System.Web.HttpCacheability.Private);
    this.Response.Cache.SetMaxAge(TimeSpan.Zero);

    var cart = ShoppingCart.GetCart(this.HttpContext);
    var cacheKey = "shooting-cart-" + cart.ShoppingCartId;
    var cachedPair = (Tuple<DateTime, int>)this.HttpContext.Cache[cacheKey];

    if (cachedPair != null) //Если данные есть в кеше на сервере
    {
        //Устанавливаем Last-Modified
        this.Response.Cache.SetLastModified(cachedPair.Item1);

        var lastModified = DateTime.MinValue;

        //Обрабатываем Conditional Get
        if (DateTime.TryParse(this.Request.Headers["If-Modified-Since"], out lastModified)
                && lastModified >= cachedPair.Item1)
        {
            return new NotModifiedResult();
        }

        ViewData["CartCount"] = cachedPair.Item2;
    }
    else //Если данных нет в кеше на сервере
    {
        //Текущее время, округленное до секунды
        var now = DateTime.Now;
        now = new DateTime(now.Year, now.Month, now.Day,
                            now.Hour, now.Minute, now.Second);

        //Устанавливаем Last-Modified
        this.Response.Cache.SetLastModified(now);

        var count = cart.GetCount();
        this.HttpContext.Cache[cacheKey] = Tuple.Create(now, count);
        ViewData["CartCount"] = count;
    }

    return PartialView("CartSummary");
}


Конечно такой код в production писать нельзя, надо разбить на несколько функций и классов для удобства сопровождения и повторного использования.

Итоговый результат на минутном забеге — 321 страница в секунду, в 5,3 раза выше, чем в первоначальном варианте.

Заключение

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