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