Посты в этой серии:
- Список задач для проверки навыков.
- Создание задачи таймера.
- Использование подходящих классов.
- Передача команд задаче таймера.
- Взаимодействие веб-фронтэнда с задачами таймера (этот пост).
В прошлом посте был показан способ создать 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. Ни в коем случае не надо писать модификацию конфига самостоятельно, для этого уже есть инструменты в студии.
Ну вот и все…
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