среда, 22 октября 2014 г.

Своё контекстное меню в JsGrid


Часто возникающая задача – это прикрутить к ячейкам JsGrid контекстное меню с некоторыми дополнительными функциями. Мой любимый пример – это контекстный фильтр, когда кликаешь по ячейке правой мышой, и жмешь фильтровать по значению ячейки. Очень удобно.

В прошлом посте я объяснял, как устроена система событий JsGrid, и оказывается, как раз с помощью события SP.JsGrid.EventType.OnRightClick - можно решить задачу с контекстным меню. Помимо события, впрочем, надо еще уметь само контекстное меню создавать. И оказывается, JsGrid имеет API и для этого тоже!

SP.JsGrid.ContextMenu


Класс ContextMenu имеет следующее определение:

/** Create a context menu. Uses the SharePoint OOTB context menus under the hood. */
export class ContextMenu {
    /** Create a context menu object.
        parentNode is the DOM node there the context menu will be attached. */
    constructor(parentNode, id);
    /** Gets the underlying SharePoint context menu object.
        Usually not necessary. */
    GetMenuElement(): any;
    EnableCompactMode(): void;
    /** Determine if the menu is currently shown */
    IsOpen(): boolean;
    /** Determine if any items were added into the menu */
    IsEmpty(): boolean;
    /** Insert a new menu item into the menu */
    InsertMenuItem(text: string, fnItemClicked: { (): void }, imageUri: string, imageAltText: string, bDisabled: boolean, bChecked: boolean): MenuItem;
    /** Insert a separator element into the menu */
    InsertSeparator(): void;
    /** Hide the menu */
    Hide(): void;
    /** Set a callback that will be fired when menu is destroyed */
    SetOnMouseOut(optfnOnMouseOut): void;
    /** Show the menu at a specified position */
    Show(pos: { top: number; left: number; width: number; height: number; }, optfnOnMouseOut): void;
    /** Show the menu next to a specified element */
    ShowAttached(elem, optfnOnMouseOut): void;
    /** Refresh menu */
    Refresh(): void;
    /** Destroy menu */
    Dispose(): void;
}
export class MenuItem {
    /** Changes state of the item. If item is not enabled, it is grayed out and can't be clicked. */
    Enable(bEnable: boolean): void;
    /** Changes checked state of the item. Checked items are displayed with a checkmark to the left. */
    Check(bCheck: boolean): void;
}

Главные методы здесь – это InsertMenuItem, который добавляет элемент в меню; и Show, который собственно меню отображает.

Алгоритм создания простейшего контекстного меню выглядит примерно так:

var m = new SP.JsGrid.ContextMenu(document.getElementById('spgridcontainer_WPQ2'), "my_jsgrid_menu");
m.InsertMenuItem("test1", function () { alert('test 1'); }, null, null, false, false);
m.InsertMenuItem("test2", function () { alert('test 2'); }, null, null, false, false);
m.InsertSeparator();
m.InsertMenuItem("test3", function () { alert('test 2'); }, null, null, false, false);
m.Show({ top: 150, left: 100, width: 0, height: 0 }, null);

Результат выполнения этого кода из консоли:

image


Позиционирование


Правильное кроссбраузерное позиционирование контекстного меню можно реализовать, используя код, который сам SharePoint использует для этого же, и который я предусмотрительно подсмотрел :)

Для этого используются функции SP.Internal.DomElement.GetLocation и SP.Internal.DomElement.GetEventLocation:

static GetLocation(element: HTMLElement): { x: number; y: number; };
static GetEventLocation(eventInfo: any): { x: number; y: number; };

GetEventLocation нужен, чтобы определить абсолютную позицию щелчка мыши. eventInfo берется из EventArgs, которые для события OnRightClick выглядят, кстати, следующим образом:

export class Click implements IEventArgs {
    constructor(eventInfo, context: JsGrid.ClickContext, recordKey: number, fieldKey: string);
    eventInfo: any;
    context: JsGrid.ClickContext;
    recordKey: number;
    fieldKey: string;
}

Обратите внимание, используется класс SP.JsGrid.EventArgs.Click, этот класс общий для типов событий OnRightClick и OnDoubleClick.

Т.е. код для позиционирования будет примерно следующий:

function OnRightClick (args) {

    // create menu here

    var parentLoc = SP.Internal.DomElement.GetLocation(containerElement);
    var eventLoc = SP.Internal.DomElement.GetEventLocation(args.eventInfo);
    myContextMenu.Show({
        left: eventLoc.x - parentLoc.x,
        top: eventLoc.y - parentLoc.y,
        width: 0,
        height: 0
    }, null);

}

Дошлифовка


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

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

Определить, что пользователь кликнул именно по ячейке, а не по заголовку – получилось совсем просто. У SP.JsGrid.EventArgs.Click есть свойство context:

export enum ClickContext {
    SelectAllSquare,
    RowHeader,
    ColumnHeader,
    Cell,
    Gantt,
    Other
}

После добавления проверки args.context == SP.JsGrid.ClickContext.Cell первая проблема была решена.

С второй проблемой помог справится метод SelectCellRangeByKey объекта JsGridControl. Причем, метода, чтобы выделить только одну ячейку – я не нашел. Есть только выделение диапазона. Впрочем, диапазон может состоять и из одной ячейки.

/** Select cells by recordKeys. Pass bAppend=true if you want to append this selection to the current rather than replace it. */
SelectCellRangeByKey(recordKey1: string, recordKey2: string, colKey1: string, colKey2: string, bAppend: boolean, optPaneId?): void;

Впрочем, тут оказалось всё не так просто. Событие OnRightClick срабатывает на mousedown, а браузерное нативное контекстное меню (как минимум в Chrome) открывается по mouseup. Из-за того, что грид переводит некоторые элементы в режим редактирования при их выделении (в частности, это касается любых колонок с dropdown’ом – Lookup, Boolean и т.д.), получалось, что выскакивало 2 контекстных меню: браузерное и моё кастомное:

image

Неприятненько!

Справиться с этой проблемой удалось путем запрета редактирования в гриде на время открытия контекстного меню. Методы, запрещающие и разрешающие редактирование грида, нашлись в JsGridControl:

/** Enables grid editing */
EnableEditing(): void;
/** Disables grid editing: all the records become readonly */
DisableEditing(): void;

DisableEditing я вызывал сразу перед тем, как выделить ячейку, а EnableEditing – через callback onfnoptMouseOut, который срабатывает именно в момент уничтожения меню:
/** Show the menu at a specified position.
    optfnOnMouseOut callback fires when the menu is destroyed. */
Show(pos: { top: number; left: number; width: number; height: number; }, optfnOnMouseOut?: { (): void; }): void;

Итоговый код



Вот что у меня получилось в итоге:

(function () {

    function init() {

        SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
            OnPostRender: jsGridCustomize
        });

    }

    function jsGridCustomize(ctx) {
        if (ctx.enteringGridMode || !ctx.inGridMode)
            return;

        var containerElement = document.getElementById('spgridcontainer_' + ctx.wpq);
        var jsGridControl = containerElement.jsgrid;
        jsGridControl.AttachEvent(SP.JsGrid.EventType.OnRightClick, function (args) {

            if (args.context != SP.JsGrid.ClickContext.Cell)
                return;

            var myContextMenu = new SP.JsGrid.ContextMenu(containerElement, "my_jsgrid_menu");
            myContextMenu.InsertMenuItem("test1", function () { alert('test 1'); }, null, null, false, false);
            myContextMenu.InsertMenuItem("test2", function () { alert('test 2'); }, null, null, false, false);
            myContextMenu.InsertSeparator();
            myContextMenu.InsertMenuItem("test3", function () { alert('test 2'); }, null, null, false, false);

            var parentLoc = SP.Internal.DomElement.GetLocation(containerElement);
            var eventLoc = SP.Internal.DomElement.GetEventLocation(args.eventInfo);
            myContextMenu.Show({
                left: eventLoc.x - parentLoc.x,
                top: eventLoc.y - parentLoc.y,
                width: 0,
                height: 0
            }, function () {
                    jsGridControl.EnableEditing();
                });


            jsGridControl.DisableEditing();
            jsGridControl.SelectCellRangeByKey(args.recordKey, args.recordKey, args.fieldKey, args.fieldKey, false);

        });

    }

    RegisterModuleInit(SPClientTemplates.Utility.ReplaceUrlTokens("~site/Style Library/jsgridCustomize.js"), init);
    init();

})();

Заключение


Оооо, я бы очень хотел чтобы все мои статьи по SharePoint были настолько простыми и незамысловатыми, и лишенными всяческих хаков! :)

В общем, с контекстными меню в JsGrid всё без проблем. Пользуем! :)

вторник, 21 октября 2014 г.

Система событий JSGrid

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

понедельник, 20 октября 2014 г.

Выделение цветом важных данных… в JSGrid!

Пока дефинишены пишутся, решил написать про некоторые простые методы работы с JSGrid, на примере подсветки данных в гриде.

Также в статье приводится довольно много сведений общего плана про JsGrid, которые очень важны для понимания, как это все работает.

вторник, 14 октября 2014 г.

Документирую SharePoint JSGrid

Тружусь сейчас над TypeScript-дефинишенами для JsGrid. Работа долгая и тяжкая: для каждой-каждой функции в JS файле из 24.5k строк нужно определить типы параметров и возвращаемого значения, понять что эта функция делает, и все это описать. Потратил уже несколько дней, по 5+ часов, и пока еще только в начале пути... Между прочим, даже главный шарепойнтовский файл, SP.debug.js, имеет меньший размер - ~21k строк...

Скриншот процесса:


суббота, 11 октября 2014 г.

Ускоряем SharePoint-разработку

SharePoint особенно знаменит затыками "на последнем километре" и багами в совершенно неожиданных местах. Поэтому чем больше решение тестируется, и чем раньше начинаешь тестировать - тем лучше.

Однако, тестировать SharePoint-решения не так-то просто. Их ведь надо еще задеплоить. А пока деплоишь, заснуть можно!... И потом сиди вспоминай, что вообще протестировать-то хотел...

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

Смешно сказать, но я до сих пор вспоминаю SPVisualDev, расширение для Visual Studio для работы с SharePoint 2007. Мгновенное развертывание в 12 hive, отличная поддержка удаленной разработки и отладки, всё удобно, быстро и просто. Этого в SharePoint Developer Tools нет и в помине даже сейчас, в 2014!...

Да, к сожалению, SharePoint с точки зрения скорости разработки сильно далеко от того, что сейчас происходит во "внешнем мире". А происходит, к слову, там, вот что:

Тем временем во "внешнем мире"...

  1. Проект IDE Light Table набрал $300k на Kickstarter. Основная идея проекта - в "живой разработке", когда IDE всегда находится в режиме отладки (!). Т.е. обычного режима не-отладки просто не бывает. Только начали писать код - тут же можно смотреть состояние переменных, и т.д.
  2. Многие современные IDE поддерживают режим "live development". Пример - опенсурсная IDE Brackets. Вот одноминутная демка:

  3. Visual Studio и ASP.Net развивают проект "Browser Link". Идея состоит в том, чтобы инжектить Signal-R скрипт на страницу, что позволяет в дальнейшем этой страницей управлять: релоадить целиком или частями, изменять, и т.д. "Browser Link" можно расширять с помощью Visual Studio extensions. В составе Web Essentials есть несколько крайне интересных расширений такого плана. Скотт Хансельман в этом 4х-минутном видео подробно все это демонстрирует:

  4. Visual Studio "14" CTP 4 по умолчанию использует open source компилятор Roslyn, который имеет расширенные средства для работы с неполными синтаксическими деревьями, благодаря чему им удалось сделать on-fly incremental compilation. Идея в том, чтобы пропустить шаг Build. Т.е. меняем код, сохраняем, переходим в браузер, F5 - и смотрим изменения.
  5. Помимо общеизвестного jsFiddle, появляется все больше и больше других Fiddle'ов, крайне интересных! Например, проект dotNetFiddle не только позволяет писать C# код онлайн, но также предоставляет режим, в котором можно тестировать полномасштабное ASP.Net MVC приложение (выберите Project Type: MVC)!


И т.д., список можно продолжать. Но я думаю вы поняли идею: все осознают, что чем меньше задержка между написанным кодом и увиденным результатом, тем лучше. И к этому идут. Но что можем сделать мы, SharePoint-разработчики?

Оказывается, и в SharePoint тоже можно сделать немало!


Для начала, продолжая мысль из поста про скрытую прелесть provider-hosted Apps, ну как бы все современные вещи можно там использовать без ограничений :) Так что всё что выше описано, нам по большому счету тоже доступно. Надо просто не забыть воспользоваться.

Во-вторых, кратко вернусь к SPVisualDev. Это open source расширение было написано ОДНИМ человеком в 2008 году. Получается, не настолько уж это и сложно?... ;)

Дальше. Давайте подумаем про BrowserLink. Можно ли что-то похожее использовать в SharePoint? Я думаю - да. Ведь BrowserLink это ни что иное, как банальный SignalR. Если заинжектить аналогичный SignalR скрипт в masterpage SharePoint-сайта, то мы получим точно такое же "живое" соединение Visual Studio и SharePoint. Я думаю можно просто выдрать из страницы то, что Visual Studio инжектит, и запихнуть в SharePoint, и оно будет работать. Не успел проверить пока.

Дальше. А что насчет jsFiddle и ASP.Net MVC fiddle? Можно ли сделать собственный SharePoint fiddle? ДА БЕЗ ПРОБЛЕМ! Вот например оцените, проект JSON fiddle:
Опять же. Сделано буквально на коленке, какой-то мужик посидел пару вечеров и написал. Вы, дорогой читатель, тоже могли бы такое написать, даже в одиночку. И вас, дорогие читатели, смею заметить, дофига.

Моя попытка: CamlJs-Console


Вообще, на удивление, многие вещи не настолько сложные, как кажутся. Примерно в середине сентября я начал работать над крайне интересным проектом под названием CamlJs-Console. Это расширение для хрома, позволяющее создавать CamlJs-запросы в режиме live development, очень похожем на пример выше про Brackets. Пишешь - и сразу же видишь результат.

Оказалось, что Chrome Extensions пишутся на HTML+CSS+JavaScript. Очень просто. При желании можно даже создавать Chrome Extension + Web App (или даже SharePoint App) из одного исходного кода...

БЛИН! Я когда только-только сделал самую первую версию у себя на локальной машине в 3 часа ночи :), я сидел тестировал и не мог остановиться. Это настолько отличалось от вот этого "поправил-5 минут деплой", что чувствовалось как магия какая-то. Серьезно :)

Общий вид:



А вот live preview данных из списка:


Мне даже удалось подцепить туда интеллисенс (на основе TypeScript Language Service - ведь как известно, TS является расширением над JS, т.е. любой валидный JS-код является также и валидным TS-кодом):


И более того, мне даже удалось сделать интеллисенс лучше, чем TS-интеллисенс в Visual Studio! Потому что ведь у меня были живые списки. И их поля... :)


Мне кажется, получилось здорово! И заняло каких-то пару недель, причем большую часть времени возился с интеллисенсом.

Преимуществ у Chrome Extension немало: не надо аутентифицироваться, не надо ничего ставить на сайт (в отличие от того же SharePoint App). Можно тестировать и онлайн сайты, и на dev виртуалке, и что особенно ценно - зайти на production и там проверить запрос, на реальных данных (!). Интеграция работает через JSOM, но можно реализовать вариант на веб-сервисах (через SPServices например), и тогда будет работать даже с SharePoint 2007 (сейчас только SP2010 и SP2013).

Что дальше?


Ооооо... у меня полно идей, просто миллион! Можно делать extension'ы наподобие CamlJs - для работы с конкретным API, решения конкретных проблем. Можно делать более широкопрофильные, наподобие JSOM fiddle, но именно в виде Chrome extension. Можно копать в сторону интеграции Visual Studio и SharePoint, создания чего-то похожего на BrowserLink. Можно подумать о гибридных решениях, Visual Studio -> Chrome Extension -> SharePoint.

И это все может значительно улучшить наш процесс работы с SharePoint. Значительно!

Коллега с работы делает сейчас потрясающую штуку, как раз под впечатлением от CamlJs-Console. Крайне легко делается, но возможности открываются потрясающие. Смысл в том, что в Chrome через F12 можно редактировать файлы, подгруженные на страницу. Но конечно эти изменения никуда не сохраняются. Ну а вот он сделал так, что сохраняются, обратно в SharePoint :) И кода там - 50 строк :))). Проект пока на супер-начальной стадии, поэтому если захочется потестировать, тестируйте аккуратно и только на dev-окружении please :) Ссылка вот.

Заключение


Я из всей этой истории главное вот что понял: все в наших руках. Не надо ждать MS или еще кого. Мы сами можем.

P.S. Если у кого есть идеи/мысли на этот счет, с удовольствием поучаствую в обсуждении - кидайте комментарии! :)

понедельник, 22 сентября 2014 г.

Загадочно бесполезный SPList.GetDataTable

Копал SPQuery в dotPeek, и наткнулся на интересный кусок кода:

if (this.m_Query.ConsiderManagedPipe
    && this.m_Query.SafeArrayFlags == null 
    && (this.m_Query.CalendarDate == DateTime.MinValue && !this.m_Query.IncludeMandatoryColumns) 
    && (this.m_Query.ViewFieldsOnly 
        && (this.m_Query.DataTableOptions & SPListGetDataTableOptions.RetrieveLookupIdsOnly) != SPListGetDataTableOptions.None 
        && ((this.m_Query.DataTableOptions & SPListGetDataTableOptions.UseBooleanDataType) != SPListGetDataTableOptions.None 
        && (this.m_Query.DataTableOptions & SPListGetDataTableOptions.UseCalculatedDataType) != SPListGetDataTableOptions.None)) 
    && (!string.IsNullOrEmpty(this.m_Query.ViewFields) 
    && string.IsNullOrEmpty(this.m_Query.ViewAttributes) 
    && (this.m_List.BaseType != SPBaseType.DiscussionBoard && !this.QueryIncludesMultiValueLookup(this.m_Query.ViewFields)) 
    && !this.m_List.HasUniqueScopes))
{
      ULS.SendTraceTag(963012918U, (ULSCatBase) ULSCat.msoulscat_WSS_Database, ULSTraceLevel.Verbose, "SPListItemCollection.EnsureListItemData: Retrieving data through the managed pipe.");
      this.m_bUseManagedPipe = true;
}

Интересно, думаю. Неспроста! Сами посмотрите: условия все такие, логически связанные с performance. Может это какой-нибудь хитрый performance-boost такой?

Ну, начал разбираться...

суббота, 20 сентября 2014 г.

3 простых способа подружить KnockoutJs и SharePoint

Попал на старый проект, SharePoint 2010. Бывает, что тут сделаешь! Спасаюсь от скуки только благодаря KnockoutJs :) Всвязи с чем - этот пост.

KnockoutJs как MVVM фреймворк на самом деле ничем не уступает тому же Angular'у. Единственная вещь, которую нужно очень хорошо изучить и вызубрить в Knockout - это когда скобки () нужны, а когда нет, и как этот ko observable wrapping вообще работает. Преодолев этот порог, больше с Knockout проблем не возникает. Angular конечно помощнее, более фундаментальный, но и всякой магии в нем намного больше, траблшутить крайне сложно, и модель освоения Angular действительно вот такая (проверено на собственном опыте):



Так что, в своих SharePoint-проектах я преимущественно использую именно Knockout. И в этом посте я опишу три простых техники использования Knockout совместно с SharePoint.