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

Наиболее часто программисты 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.

Теги : SharePoint