English version

Цикличность и временное отключение приемников событий в SharePoint

Как Вы можете знать,  в SharePoint существует множество приемников событий (EventReceiver), которые позволяют вызывать пользовательский код  при выполнении стандартных операций с объектами SharePoint, например добавление/удаление/изменение элементов списков.  Работа с приемниками событий заслуживает отдельной статьи, но их по данной тематике и так довольно много, например Тут

Сегодня мы рассмотрим 2 частных случая проблем, которые могут возникнуть у начинающего разработчика SharePoint при работе с ресиверами:

1. Цикличный вызов событий.

Довольно легко можно представить ситуацию, когда ресивер может “загнать” себя в бесконечный цикл. Для примера возьмем приемник событий для элемента списка “После обновления” (ItemUpdated) . Ваш код выполняет дополнительные действия с данным элементом (например, на основе введенных пользователем данных, производите дополнительные вычисления и записываете их в требуемое поле), после чего, конечно же, вызываете Update для сохранения данных. После вызова что должно произойти? Конечно. Снова вызовется Ваш код, который был описан в ресивере . И так бесконечно.

public override void ItemUpdated(SPItemEventProperties properties)
{
	base.ItemUpdated(properties);
	SPListItem Item = properties.ListItem;

	Item["Title"] = DateTime.Today.ToString("dd.MM.yyyy");
	Item.Update();
}

По непонятным мне причинам, часть разработчиков считает, что для отключения выполнения обработчиков событий достаточно вызывать SystemUpdate вместо Updateу объекта . Но это не так. Мало того, что на msdn говорится о том, что данный метод позволяет обновлять элементы списка без обновления полей Изменено (Modified), Кем изменено(ModifiedBy)  и увеличению версии элемента, так и простым экспериментальным путем доказывается обратное.

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

Данную проблему поможет нам решить свойство  EventFiringEnabled у SPEventReceiverBase (или даже у текущего наследуемого объекта от SPEventReceiverBase).

Данное свойство позволяет управлять возможностью вызова каких-либо обработчиков событий унаследованных от класса SPEventReceiverBase в текущем потоке. То есть в Вашем коде Вы можете выключать и включать вызовы обработчиков событий и не бояться, что другой код или простая работа пользователя  (которая также вызывает ресиверы) будет омрачена отключением или выключением обработчиков в Вашем конкретном методе.

public override void ItemUpdated(SPItemEventProperties properties)
{
	base.ItemUpdated(properties);
	SPListItem Item = properties.ListItem;
	this.EventFiringEnabled = false;
	Item["Title"] = DateTime.Today.ToString("dd.MM.yyyy");
	Item.Update();
	this.EventFiringEnabled = true;
}

При вызове указанного кода при выполнении метода вызов ресиверов будет отключен. Стоит обратить внимание на то, что все ресиверы, унаследованные от SPEventReceiverBase, при установке свойства EventFiringEnabled в false будут отключены и об этом необходимо будет позаботиться самим. То есть  Если в промежутке кода между выключением вызова ресиверов и включением Вы попытаетесь обновить другой список, к которому привязан обработчик событий на обновление, он не выполнится (Почему? Об этом немного ниже). Код, который должен был быть выполненным, необходимо выполнить принудительно.

Если обратить внимание на указанный выше код, то станет сразу понятно, что он не оптимален. Как минимум логичнее обернуть его в try-catch-finally(ну или как минимум просто try-finally). Вы же знаете что это или это?

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

public override void ItemUpdated(SPItemEventProperties properties)
{
	base.ItemUpdated(properties);
	try
	{
		SPListItem Item = properties.ListItem;
		this.EventFiringEnabled = false;
		Item["Title"] = DateTime.Today.ToString("dd.MM.yyyy");
		Item.Update();
	}
	finally
	{
		this.EventFiringEnabled = true;
	}
}

Ниже по тексту будет представлен еще один возможный вариант отключения с инструкциейusing

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

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

По своей сути задача очень схожа с предыдущей, но у нас нет объекта  SPEventReceiverBase и отключать вызов ресиверов нечему.  Тут необходимо обратиться к рефлетору и посмотреть, что из себя представляет данный класс и как нам дальше быть.

protected bool EventFiringEnabled
{
  get
  {
	return !SPEventManager.EventFiringDisabled;
  }
  set
  {
	SPEventManager.EventFiringDisabled = !value;
  }
}

Из кода видно, что у класса есть свойство EventFiringEnabled (мы им чуть ранее пользовались для отключения вызова событий) и с его помощью получается или устанавливается значение из статического свойства SPEventManager.EventFiringDisabled. Код данного свойства представлен ниже:

internal static bool EventFiringDisabled
{
  get
  {
	SPEventManager.EnsureTlsEventFiringDisabled();
	object data = Thread.GetData(SPEventManager.m_tlsEventFiringDisabled);
	return data != null && (bool) data;
  }
  set
  {
	SPEventManager.EnsureTlsEventFiringDisabled();
	Thread.SetData(SPEventManager.m_tlsEventFiringDisabled, (object) (bool) (value ? 1 : 0));
  }
}

Так как метод статический, а внутренняя работа основана уже на потоках, то получается, что каждый унаследованный от SPEventReceiverBase класс работает уже не совсем с контекстом текущего объекта. Требуемая информация читается и записывается в отведенные для наших потоков ячейках памяти. Получается, что не столько важно из какого ресивера выполняется отключение или включение вызовов других обработчиков, сколько важно в каком потоке это делается. Таким образом, достаточно создать собственный класс, унаследованный от   SPEventReceiverBase (или от SPItemEventReceiver для нашего конкретного случая, который в свою очередь также является наследником ), в требуемом месте кода инициализировать экземпляр и работать с уже привычным нам свойством EventFiringEnabled.

public class DisableItemEvents : SPItemEventReceiver
{
	public bool CustomEventFiringEnabled
	{
		get { return base.EventFiringEnabled; }
		set { base.EventFiringEnabled = value; }
	} 
}

И вызывать , например, следующим образом:

var EventsDisable = new DisableItemEvents();
try
{
	Item["Title"] = DateTime.Today.ToString("dd.MM.yyyy");
	EventsDisable.CustomEventFiringEnabled = false;
	Item.Update();
}
finally
{
	EventsDisable.CustomEventFiringEnabled = true;
}

Получилось то, чего мы и ожидали. При обновлении не отработал ресивер. И как я сказал ранее, данный подход имеет право на жизнь и многие таким пользуются, но данную конструкцию я бы описал немного иначе. Мне очень нравятся using-паттерны. Как минимум за то, что используя данный функционал можно быть уверенным, что независимо от ошибок внутри инструкции, всегда будет вызван Dispose.

Свой класс я описал следующим образом:

public class DisableItemEvents : SPItemEventReceiver, IDisposable
{
	private bool _EventStatus;

	public DisableItemEvents()
	{
		_EventStatus = base.EventFiringEnabled;
		base.EventFiringEnabled = false;
	}

	public void Dispose()
	{
		base.EventFiringEnabled = _EventStatus;
	}

}

Работа с данным классом получается следующей:

using (new DisableItemEvents())
{
	Item.Update(); // Вызов ресиверов НЕ происходит
}
Item.Update(); // Вызов ресиверов происходит

Здесь уже комментарии излишни, но тем не менее. При инициализации объекта DisableItemEvents будет сохранено первоначальное значение свойства EventFiringEnabled и установлено в false. А при диспоузе объекта свойство EventFiringEnabled будет возвращено обратно.

Таким образом мы рассмотрели возможные варианты отключения вызовов обработчиков событий как внутри ресиверов, так и из вне.


Опубликовано: 19.03.2014
Автор: Сергей Снитко