Посты в этой серии:

  1. Список задач для проверки навыков.
  2. Создание задачи таймера.
  3. Использование подходящих классов.
  4. Передача команд задаче таймера.
  5. Взаимодействие веб-фронтэнда с задачами таймера (этот пост).

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

Теперь необходимо придумать как оправлять эти сообщения. В 2010 есть почти универсальный способ для размещения функционала – Ribbon. Даже если у вас возникает непреодолимое желание сделать ссылку\кнопку\пункт меню в другом месте, то скорее всего для целей usability надо будет повторить его в ribbon.

Для того чтобы добавить кнопку в ribbon надо создать custom action, я для этого использую cks:dev. Руководство по расширению риббона можно найти как на MSDN, так и в блогах глубоко уважаемых людей (надеюсь меня не забанят за такое количество ссылок).

Мой код получился такой:

<CustomAction Id="CleanupLibraryButton"
              Title="Cleanup"
              RegistrationType="List"
              RegistrationId="101"
              Location="CommandUI.Ribbon">
  <CommandUIExtension>
    <CommandUIDefinitions>
      <CommandUIDefinition Location="Ribbon.Library.Settings.Controls._children">
        <Button Id="Ribbon.Library.Settings.Cleanup"
                Command="CleanupLibraryCommand"
                TemplateAlias="o2"
                LabelText="Cleanup"
                Sequence="100"
                Image16by16="/_layouts/images/warning16by16.gif"
                Image32by32="/_layouts/images/CRIT_32.GIF"                   
                />
      </CommandUIDefinition>
    </CommandUIDefinitions>
    <CommandUIHandlers>
      <CommandUIHandler Command="CleanupLibraryCommand" 
                        CommandAction="???"/>
    </CommandUIHandlers>
  </CommandUIExtension>
</CustomAction>

Немного громоздко, но если разобраться в схеме, то довольно очевидно. Кнопка добавляется в последнюю группу кнопок на закладке “Билиотека”.

Теперь самый интересный вопрос, что написать в CommandAction, где я в схеме поставил вопросы.

Самый прямолинейный способ – указать ссылку на application page, который вызовет site.AddWorkItem, но это кардинально противоречит самой идее ribbon.

 

SharePoint 2007 way

В SharePoint 2007 часто применялся delegate control, и действия в интерфейсе выполняли postback, а этот самый контрол обрабатывал форму и производил действия.

Для начала добавлю еще один cutom action с location равным ScriptLink. В таком custom action можно размещать ссылку на javascript или блок кода, который будет выведен на каждой странице.

<CustomAction 
    Location="ScriptLink"
    ScriptSrc="~site/_layouts/CleanupTimerJob/script.js" />

Команда для ribbon будет выглядеть так:

<CommandUIHandler 
    Command="CleanupLibraryCommand" 
    CommandAction="javascript:submitLibraryToCleanup()"/>

В самом же js файле будет простой код:

function submitLibraryToCleanup() {
    var listId = SP.ListOperation.Selection.getSelectedList();
    if (listId) {
        __doPostBack('CleanupPostBackEvent', listId);
    }
}

SP.ListOperation.Selection – класс, позволяющий получать данные о текущем представлении списка. Представлений списка на странице может быть больше одного, а ribbon – один.

Функция __doPostBack создается веб-формами, в первом параметре обычно указывается id элемента управления, которому адресуется postback, а во втором передаются параметры. Но узнать id нашего delegate control (который будет ниже) не представляется возможным, поэтому требуется заранее определенная константа и ручной анализ в теле контрола.

Сам контрол:

[Guid("0a3b8df1-0a43-485d-be83-983c1df8b30d")]
public class PostbackHandler : WebControl
{
    protected override void OnLoad(EventArgs e)
    {
        if (this.Page.Request["__EVENTTARGET"] == Constants.CleanupPostBackEvent)
        {
            var listId = new Guid(this.Page.Request["__EVENTARGUMENT"]);
            var web = SPContext.Current.Web;
                            
            SPSecurity.RunWithElevatedPrivileges(() =>
                {
                    using (var site = new SPSite(SPContext.Current.Site.ID))
                    {
                        site.AddWorkItem(
                            new Guid(), DateTime.UtcNow, Constants.WorkItemType,
                            web.ID, listId, -1,
                            true, new Guid(), web.ID,
                            web.CurrentUser.ID, null, null, new Guid());
                    }
                });
        }
    }
}

Основное внимание стоит уделить функции AddWorkItem. Во-первых её необходимо вызывать с правами администратора семейства сайтов, иначе будет ошибка. Во-вторых при указании времени доставки WorkItem необходимо указывать время в формате UTC. В-третьих, даже если не используются параметры listItemId и  userId, то все равно надо указывать ненулевые значения иначе SharePoint попытается записать в базу null, что приведет к ошибке так как эти поля not null. И в-четвертых лучше всего указывать batchId, такой же как у webId, если нет других соображений. Судя по документации msdn и реализации TimerJobUtility это может сэкономить ресурсы.

Чтобы ваш delegate control заработал необходимо добавить элемент в фичу. Здесь также поможет cks:dev, в нем уже есть шаблон.

<Control Id="AdditionalPageHead" 
         Sequence="1000" 
         ControlAssembly="$SharePoint.Project.AssemblyFullName$" 
         ControlClass="$SharePoint.Type.0a3b8df1-0a43-485d-be83-983c1df8b30d.FullName$" />

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

image

Ну вот и все…

SharePoint 2010 way

Если сделать как написано выше, то нажатие на кнопку приведет к постбеку, то есть перезагрузке всей странице. Хотя, учитывая архитектуру, пользователь сразу не увидит изменений в любом случае. Поэтому необходим ajax, обязательно с feedback пользователю.

Для этого нужно сделать: веб-сервис в sharepoint, который обработает запрос и поменять script.js, чтобы он вызывал этот веб-сервис.

Чтобы сделать веб-сервис снова нужно воспользоваться cks:dev с готовым шаблоном. В готов шаблоне надо поменять в svc файле тип фабрики с MultipleBaseAddressBasicHttpBindingServiceHostFactory на MultipleBaseAddressWebServiceHostFactory чтобы можно было вызывать методы сервиса из js.

Выкинув все лишнее можно получим до безобразия простой сервис:

[Guid("36471285-d168-49ea-b191-6c83cfe1fe3e")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
[ServiceContract]
public class CleanupService
{
    [OperationContract]
    [WebInvoke(
        UriTemplate = "/SubmitLibraryToCleanup({listId})",
        BodyStyle = WebMessageBodyStyle.Bare,
        RequestFormat = WebMessageFormat.Json,
        ResponseFormat = WebMessageFormat.Json)]
    public void SubmitLibraryToCleanup(string listId)
    {
        CleanupUtility.AddCleanupWorkitem(SPContext.Current.Web, new Guid(listId));
    }
}

Как видите, большую часть сервиса занимают атрибуты. Метод вызывается с помощью POST, параметры передаются прямо в строке запроса.

Далее необходимо в script.js файле поправить метод submitLibraryToCleanup чтобы он вызывал веб-сервис.

ВНИМАНИЕ. Приведенный далее код работает, но крайне не рекомендую использовать его в своих решениях. Используйте более человеческие библиотеки, вроде jQuery.

function submitLibraryToCleanup(isOldSchool) {
    var listId = SP.ListOperation.Selection.getSelectedList();
    if (listId) {
        var notification = null;
        var request = new Sys.Net.WebRequest();
        request.set_url(GetWebUrl() + "_vti_bin/CleanupTimerJob/CleanupService.svc/SubmitLibraryToCleanup("+listId+")");
        request.set_httpVerb("POST");
        request.add_completed(function(executor, eventArgs) {
            SP.UI.Notify.removeNotification(notification);
            SP.UI.Notify.addNotification("Done",false);
        });             
        notification = SP.UI.Notify.addNotification("Submitting library to cleanup",true);
        request.invoke();
    }
}

Этот код использует так называемую Microsoft Ajax Library для выполнения запроса на сервер, и методы Client OM SharePoint для отображения оповещений.

Функция GetWebUrl скопирована отсюда.

Заключение

Надеюсь вы все таки дочитали до сюда и узнали что-то новое. Весь код можно найти на spsamples.codeplex.com

Теги : js, SharePoint