Об обработчиках событий элементов списка, часть 2

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

Наиболее часто программисты SharePoint сталкиваются с необходимостью реализовать field-level security, так как встроенных механизмов в SharePoint нет (зато в Dynamics CRM есть, если что).  Можно написать кастомные формы, которые на уровне интерфейса блокируют возможности поправить значения элементов, но учитывая богатые клиентские возможности SharePoint 2010 такие ограничения легко обойти.

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

Первое приближение:

public static bool IsFieldChanged(this SPItemEventProperties properties, 
                                       SPField field)
{
    var after = (string)properties.AfterProperties[field.InternalName];            
    var before = Convert.ToString(properties.ListItem[field.Id]);
    
    return after != before;
}

Этот код обрабатывает довольно малое число use cases:

  1. AfterProperties может быть null если свойство не менялось (хотя при отправке формы в AfterProperties  попадают все поля)
  2. AfterProperties содержит пустую строку если значения нет, SPListItem может при этом возвращать как пустую строку, так и null

Второе приближение:

public static bool IsFieldChanged(this SPItemEventProperties properties, SPField field)
{
    var after = (string)properties.AfterProperties[field.InternalName];            
    var before = Convert.ToString(properties.ListItem[field.Id]);
    

    //AfterProperties[fieldname] == null - field not changed
    if (after == null)
    {
        return false;
    }

    //AfterProperties[fieldname] == "" - field set to null
    if (after == "" && string.IsNullOrEmpty(before))
    {
        return false;
    }

    //AfterProperties[fieldname] != "", old value is null or empty - field changed
    if (string.IsNullOrEmpty(before))
    {
        return true;
    }
    
    return after != before
}

Уже лучше, НО:

  1. Не сработает на Lookup полях, так как форма передает только Id, без Display Value
  2. Не сработает на булевых полях, так как слишком разные значения попадают в SPListItem и AfterProperties (интересно почему так?)
  3. Не сработает на датах, так как используется разное форматирование
  4. Не сработает на множественных значениях, так как они эквивалентны с точностью до порядка

Выписывать ифы на каждый тип поля или делать стратегии как-то не хочется, хочется универсальный способ. Ведь он существует, как-то сам SharePoint обрабатывает входящие текстовые значения и судя по всему без магии, так как можно создавать свои поля. MSDN быстро дает ответ – SPField.GetFieldValue.

Третье приближение:

public static bool IsFieldChanged(this SPItemEventProperties properties, SPField field)
{
    var after = (string)properties.AfterProperties[field.InternalName];            
    var before = Convert.ToString(properties.ListItem[field.Id]);
    

    //AfterProperties[fieldname] == null - field not changed
    if (after == null)
    {
        return false;
    }

    //AfterProperties[fieldname] == "" - field set to null
    if (after == "" && string.IsNullOrEmpty(before))
    {
        return false;
    }

    //AfterProperties[fieldname] != "", old value is null or empty - field changed
    if (string.IsNullOrEmpty(before))
    {
        return true;
    }

    var afterValue = field.GetFieldValue(after);
    var beforeValue = field.GetFieldValue(before);

    if (afterValue.Equals(beforeValue))
    {
        return false;
    }

    return after != before;
}

Еще лучше, будут обрабатываться даты и другие поля, для типов значений которых, реализована метод Equals. Но в SharePoint это не так:

  1. SPFieldLookupValue и SPFiledUserValue не реализуют метод Equals(похоже что  авторы и не рассматривали сценарии сравнения значений этих типов)
  2. Для сравнения множественных значений надо написать дополнительный код
public static bool IsFieldChanged(this SPItemEventProperties properties, SPField field)
{
    var after = (string)properties.AfterProperties[field.InternalName];            
    var before = Convert.ToString(properties.ListItem[field.Id]);
    

    //AfterProperties[fieldname] == null - field not changed
    if (after == null)
    {
        return false;
    }

    //AfterProperties[fieldname] == "" - field set to null
    if (after == "" && string.IsNullOrEmpty(before))
    {
        return false;
    }

    //AfterProperties[fieldname] != "", old value is null or empty - field changed
    if (string.IsNullOrEmpty(before))
    {
        return true;
    }

    var afterValue = field.GetFieldValue(after);
    var beforeValue = field.GetFieldValue(before);

    if (afterValue.Equals(beforeValue))
    {
        return false;
    }

    //Compare SPFieldLookupValue and SPFieldUserValue
    if (afterValue is SPFieldLookupValue)
    {
        return (afterValue as SPFieldLookupValue).LookupId != (beforeValue as SPFieldLookupValue).LookupId;
    }

    //Compare SPFieldLookupValueCollection and SPFieldUserValueCollection
    if (field is SPFieldLookup && (field as SPFieldLookup).AllowMultipleValues)
    {
        var hsa = new HashSet<int>((afterValue as SPFieldLookupValueCollection).OfType<SPFieldLookupValue>().Select(l => l.LookupId));
        var hsb = new HashSet<int>((beforeValue as SPFieldLookupValueCollection).OfType<SPFieldLookupValue>().Select(l => l.LookupId));
        return !hsa.SetEquals(hsb);
    }

    return  after != before;
}

Почти хорошо, осталось два момента:

  1. Не работает сравнение для SPFieldMultiChoiceValue
  2. Для RichText значений в AfterProperties попадают теги все в верхнем регистре, а в SPListItem – в нижнем

Полный код для SharePoint Foundation доступен по ссылке http://pastebin.com/8vijzvxV

Если найдете сценарии, которые не обрабатывает данный код – пишите.

PS. Для SharePoint Server необходимо обрабатывать дополнительные поля, такие как TaxonomyField.



Об обработчиках событий элементов списка

Обработчики событий элементов списка  это классы, унаследованные от SPItemEventReceiver. В классе содержится много методов, почти все они делятся на две группы: Pre-события – методы, оканчивающиеся на -ing, и Post-события – методы, оканчивающиеся на -ed. Все события принимают один аргумент экземпляр класса SPItemEventProperties.

Это теоретический минимум, который необходимо знать для начала.

Коварные AfterProperties и BeforeProperties

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

  1. Для начала необходимо запомнить, а потом распечатать и повесить на стену, на холодильник, да доску и на все остальные поверхности следующие таблицы:
    Список BeforeProperties AfterProperties properties.ListItem
    ItemAdding Пусто Новые значения null
    ItemAdded Пусто Новые значения Новые значения
    ItemUpdating Пусто Новые значения Старые значения
    ItemUpdated Пусто Новые значения Новые значения
    ItemDeleting Пусто Пусто Старые значения
    ItemDeleted Пусто Пусто null

    Библиотека BeforeProperties AfterProperties properties.ListItem
    ItemAdding Пусто Пусто null
    ItemAdded Пусто Пусто Новые значения
    ItemUpdating Старые значения Новые значения Старые значения
    ItemUpdated Старые значения Новые значения Новые значения
    ItemDeleting Пусто Пусто Старые значения
    ItemDeleted Пусто Пусто null

    Поведение списков и библиотек документов разное и отличается от предполагаемого.
    Как видите из таблиц выше, BeforeProperties почти бесполезное свойство.
  2. AfterProperties и BeforeProperties содержат свойство ChangedProperties. Это свойство показывает измененные поля в самой коллекции, а не в элементе.
  3. Свойство-индексатор AfterProperties и BeforeProperties принимает Internal Name поля, а не Display Name как в properties.ListItem.
  4. AfterProperties и BeforeProperties поддерживают нетипизированный IEnumerable, но нигде в документации не указано какого типа возвращается перечисление. Опыты показывают что возвращается DictionaryEntry.
  5. AfterProperties и BeforeProperties возвращают все значения в виде строк. Кроме того для boolean типа поля может быт возвращено "-1" в качестве значения, а строки возвращаются в универсальном формате и при парсинге автоматически переводятся в текущую локаль (+3 часа обычно получается).
  6. При изменении элемента списка в коде в AfterProperties попадают только измененные значения, а при сохранении формы – все значения формы.
  7. При сохранении формы с RichText полем в AfterProperties попадает HTML со всеми заглавными буквами в названиях тегов.

Отмена действия

Pre-события позволяют отменить действие. Для этого необходимо в properties.Cancel присвоить true, присвоить необходимые значения свойствам properties.Status и properties.ErrorMessage. Но и тут есть особенности:

  1. Если вы собираетесь отменить действие, то не вызывайте базовую реализацию метода-обработчика. Иначе отмена не произойдет.
  2. Если вы поставите статус CancelWithError, то будет выкинуто стандартное исключение, которое в режиме отладки отображается желтым экраном смерти.
  3. Если хотите показать свое сообщение об ошибке, то сделайте CancelWithRedirect, но учтите что в таком случае управление не вернется к вызывающему коду.
  4. Если необходимо выполнение разных способов отмены, то анализируйте SPContext и его свойства FormContext и ViewContext.
  5. Отмена работает всегда, для любой учетной записи, в том числе системной. Желательно позволять выполнять действие (не отменять его) администратору коллекции сайтов и учетной записи "SHAREPOINT\system".

Конкурентное выполнение обработчиков событий

В этом посте описан пайплайн обработки событий, картинка ниже кратко его иллюстрирует.

Как видно обработчики Post-событий могут запускаться параллельно. Если будут параллельно запускаться несколько обработчиков, изменяющих сам элемент, то может появиться состояние гонки. Класс SPListItem снабжен механизмом так называемой “оптимистичной конкуренции”. В случая если другой потом успел поменять значение в базе между моментом считывания и записью данных, то выпадет исключение. Но исключения в пост-обработчиках не очень эффективны, так как нет возможности как-либо сигнализировать об ошибке, если только это не синхронный обработчик.

  1. По возможности не изменяйте элемент списка из асинхронного пост-обработчика.
  2. Перехватывайте SPListDataValidationException. Если поймали такое исключение, то выполните properties.InvalidateListItem, а потом снова код обновления элемента.
  3. Используйте следующий блок кода в пост-обработчиках, чтобы не вызвать их циклического запуска
    try
    {
        this.EventFiringEnabled = false;
        //...здесь вызов Update...
    }
    finally
    {
        this.EventFiringEnabled = true;
    }
  4. Также можно использовать SystemUpdate, чтобы не вызывать обработчики событий и не менять время последнего изменения элемента.

К сожалению это далеко не все грабли, которые встречаются при обработке событий. С прочими граблями можно ознакомиться тут:http://msdn.microsoft.com/en-us/library/aa979520.aspx


Unity 2.0 Interception

Ранее я писал про механизмы AOP в Unity 1.0 (или 1.2 на тот момент). Недавно увидел этот пост, с примером для Unity 2.0.

В первой версии было фактически две возможности для задания перехватчиков вызовов: это атрибуты на классах или сложный код (или xml), задающий условия по которым будут срабатывать обработчики вызовов.

Оба варианта довольно плохие, первый заставляет править существующий код, второй просто неподъемный. В Unity 2.0 исправили ситуацию с помощью так называемых InterceptionBehavior.

Пример кода из поста в новом стиле.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.InterceptionExtension;
using System.Transactions;

namespace Unity20Interception
{
public interface IAccountService
{
    // Хотим чтобы метод Withdraw выполнялся под транзакцией,
    // Но ничего помечать не будем.
    void Withdraw(decimal amount);
}

public class AccountService : IAccountService
{
    public void Withdraw(decimal amount)
    {
        if (amount < 0 || amount > 1000)
            throw new ArgumentOutOfRangeException("amount");
    }
}

//Собственно класс обработчика вызовов
public class TransactionBehavior: IInterceptionBehavior
{

    #region IInterceptionBehavior Members

    public IEnumerable<Type> GetRequiredInterfaces()
    {
        return Enumerable.Empty<Type>();
    }

    public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        using (TransactionScope transaction = new TransactionScope())
        {
            var result = getNext()(input, getNext);

            if (result.Exception == null)
            {
                // исключений не возникло, завершим транзакцию
                transaction.Complete();
                Console.WriteLine("Transaction commited");
            }
            return result;
        }
    }

    public bool WillExecute
    {
        get { return true; }
    }

    #endregion
}

class Program
{
    static void Main(string[] args)
    {
        var container = new UnityContainer().AddNewExtension<Interception>();
        container                
            .RegisterType<IAccountService, AccountService>(
                //Устанавливаем способ перехвата
                new Interceptor<InterfaceInterceptor>(), 
                //Устанавливаем обработчик
                new InterceptionBehavior<TransactionBehavior>() 
            );

        var service = container.Resolve<IAccountService>();
        service.Withdraw(33); // транзакция пройдет успешно
        service.Withdraw(-6);
    }
}

Также новых механизм перехвата позволяет добавлять к сгенерированному классу-перехватчику необходимые интерфейсы. Например можно сделать таким образом behavior, который добавляет реализацию INotifyPropertyChanged.
http://msdn.microsoft.com/en-us/library/ff660851(v=PandP.20).aspx



SharePoint и Url форм

Постоянно вижу вопрос на форумах: “Как в SPD сделать ссылку на элемент списка?”

Ответ: Используйте listform.aspx

{webUrl}/_layouts/listform.aspx?ListId={ListId}
&PageType={PageType}
&ContentTypeId={CTId}
&ID={ItemId}
  • webUrl – урл сайта
  • ListId – GUID списка
  • PageType – тип формы, число, возможные значения тут
  • CTId – идентификатор типа содержимого, необязательный параметр, необходим для формы нового элемента
  • ItemId – ID элемента списка, необязательный параметр, необходим для форм отображения и редактирования
  • Также можно дописывать свои параметры в url, они передадутся форме

При создании форм в SPD в качестве адреса listform.aspx можно указывать выражение

<%$SPUrl:~site/_layouts/listform.aspx%>

Таким способом можно указывать только формы и представления по-умолчанию. Для специализированных форм и представлений можно прописать прямой URL.



Начинающим админам SharePoint

Что нужно почитать чтобы правильно развернуть ферму в вашей организации.

Подготовка

https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-intro.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-prep-work-1.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-prep-work-2.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-prep-work-3.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-prep-work-4.aspx

Установка


https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-installation.aspx

Настройка


https://www.nothingbutsharepoint.com/sites/itpro/Pages/life-on-the-farm-post-install-configure-part-1.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-post-install-configure-part-2.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-post-install-part-3.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Pages/Life-on-the-Farm-SP2010-Configuration---Post-Install-Setup-Part-4.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-post-install-configure-part-5.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-post-install-configure-part-6.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Pages/life-on-the-farm-post-install-configure-part-7.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Pages/life-on-the-farm-post-install-configure-part-8.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Pages/life-on-the-farm-post-install-configure-part-9.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Articles/Pages/life-on-the-farm-post-install-configure-part-10.aspx
https://www.nothingbutsharepoint.com/sites/itpro/Pages/life-on-the-farm-post-install-configure-part-11.aspx