Linq to SharePoint. Часть 5. Поля Choice и MultiChoice

Часть 1. First()/FirstOrDefault(), T-SQL IN, Path
Часть 2. Count(), Take(), Skip(), JOIN
Часть 3. Анонимный доступ, Получение списка по URL'у, Cross-Site запросы
Часть 4. SPListItem -> LINQ, Dynamic Linq to SharePoint
Часть 5. Поля Choice и MultiChoice в Linq to SharePoint
Часть 6. Сравнение производительности Linq to SharePoint и Camlex.NET

Велик и могуч Linq to SharePoint. Посему очередной пост будет посвящен работе с ним. На этот раз я покажу как работать с полями типа Choice и MultiChoice.

enum для поля типа Choice

Environment

Все примеры будут основываться на модели из 2 части постов, посвященных Linq to SharePoint. Для демонстрации в этом посте я добавил два поля в тип содержимого Employee. Первое поле - Sex (SPFieldChoice), второе - Hobbies (SPFieldMultiChoice):

  1. <Field ID="{68c1ee4a-5a25-4ccb-82ca-f5ff17e2016f}" Name="Sex" DisplayName="Sex" Type="Choice" Format="RadioButtons">
  2.   <CHOICES>
  3.     <CHOICE>Male</CHOICE>
  4.     <CHOICE>Female</CHOICE>
  5.   </CHOICES>
  6.   <Default>Male</Default>
  7. </Field>
  8. <Field ID="{BD1898B4-0869-41F2-9DB0-B0F1B8F139D3}" Name="Hobbies" DisplayName="Hobbies" Type="MultiChoice" Mult="TRUE">
  9.   <CHOICES>
  10.     <CHOICE>Chess</CHOICE>
  11.     <CHOICE>Football</CHOICE>
  12.     <CHOICE>Basketball</CHOICE>
  13.   </CHOICES>
  14.   <Default>Chess</Default>
  15. </Field>

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

  1. public enum EmployeeSex
  2. {
  3.     None = 0,
  4.     Invalid = 1,
  5.     [Choice(Value = "Male")]
  6.     Male = 2,
  7.     [Choice(Value = "Female")]
  8.     Female = 4
  9. }
  10.  
  11. [FlagsAttribute]
  12. public enum EmployeeHobby
  13. {
  14.     None = 0,
  15.     Invalid = 1,
  16.     [Choice(Value = "Chess")]
  17.     Chess = 2,
  18.     [Choice(Value = "Football")]
  19.     Football = 4,
  20.     [Choice(Value = "Basketball")]
  21.     Basketball = 8
  22. }

Single Choice

Начнем с простого случая, когда у нас поле может иметь только одно значение. Имея код, созданный SPMetal, все кажется очень просто. Если нам необходимо выбрать всех сотрудников мужского пола, то логично было бы использовать следующий код:

  1. using (var ctx = new ZhukDataContext(siteUrl))
  2. {
  3.     var employees = ctx.Employees
  4.         .Where(emp => emp.Sex == EmployeeSex.Male)
  5.         .ToList();
  6.     // ...
  7. }

Но в этом случае SPLinqProvider не сможет конвертировать этот запрос в ожидаемый нами CAML. Сначала произойдет выгрузка всех элементов, а уже потом в памяти произойдет фильтрация. Связано это с тем, что поля типа Choice и MultiChoice хранятся в базе данных в виде текстовых значений.

Чтобы обойти это ограничение я использую следующий трюк: я изменяю тип свойства с перечисления на текст. А, чтобы сохранить использование перечислений, можно использовать поле без атрибута ColumnAttribute:

  1. public EmployeeSex? Sex
  2. {
  3.     get
  4.     {
  5.         var res = Enum.Parse(typeof (EmployeeSex), _sexValue);
  6.         return res is EmployeeSex ? (EmployeeSex) res : EmployeeSex.Invalid;
  7.     }
  8. }
  9.  
  10. [Column(Name = "Sex", Storage = "_sexValue", FieldType = "Choice")]
  11. public string SexValue
  12. {
  13.     get
  14.     {
  15.         return _sexValue;
  16.     }
  17.     set
  18.     {
  19.         if ((value == _sexValue)) return;
  20.         var vals = Enum.GetValues(typeof(EmployeeSex));
  21.         foreach (EmployeeSex val in vals)
  22.         {
  23.             if (!string.Equals(Enum.GetName(typeof(EmployeeSex), val), value,
  24.                                 StringComparison.InvariantCultureIgnoreCase)) continue;
  25.             OnPropertyChanging("SexValue", _sexValue);
  26.             _sexValue = value;
  27.             OnPropertyChanged("SexValue");
  28.         }
  29.     }
  30. }

Теперь фильтровать можно по текстовому полю SexValue, получая в результате правильный CAML-запрос. А чтобы избежать использование текстовых значений в коде, можно написать метод расширитель для перечислений, который будет возвращать значение свойства Value атрибута ChoiceAttribute:

  1. public static string GetChoiceValue(this Enum enumerator)
  2. {
  3.     // Получаем тип
  4.     var type = enumerator.GetType();
  5.     // Получаем имя поля
  6.     var fieldName = Enum.GetName(enumerator.GetType(), enumerator);
  7.     // Получаем поле
  8.     var field = type.GetField(fieldName, 
  9.                 BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public);
  10.     // Получаем атрибуты найденного поля
  11.     var attributes = field.GetCustomAttributes(typeof (ChoiceAttribute), true);
  12.     var attribute = attributes.FirstOrDefault();
  13.     // Если атрибута нет, то возвращаем пустую строку
  14.     // Иначе возвращаем значение Value атрибута
  15.     return attribute == null
  16.                 ? string.Empty
  17.                 : ((ChoiceAttribute) attribute).Value;
  18. }

Вот теперь все работает правильно и выглядит довольно красиво:

  1. using (var ctx = new ZhukDataContext(siteUrl))
  2. {
  3.     var employees = ctx.Employees
  4.         .Where(emp => emp.SexValue == EmployeeSex.Male.GetChoiceValue())
  5.         .ToList();
  6. }

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

Multiple Choice

А вот в этом случае ничего уже не выйдет (по крайней мере, я не придумал, как это обойти). Убежден, что копать надо в сторону того, что все значения полей типа MultiChoice в базе данных обрамлены с обеих сторон разделителем (;#):

Значение полей Choice и MultiChoice в базе данных содержимого SharePoint 2010

Остается только привести значение перечислителя к тексту. И использовать аналог метода расширителя EqualsAny, описанный мною в 4 части. Только наш новый метод будет проверять на содержание значения в строке:

  1. public static Expression<Func<T, bool>> ContainsAny<T>(this Expression<Func<T, string>> selector,
  2.     IEnumerable<string> values)
  3. {
  4.     // Если значений нет, то возвращаем выражение x=> false
  5.     if (!values.Any()) return x => false;
  6.     // Аналогично поступаем в случае, когда кол-во параметров не равно одному
  7.     if (selector.Parameters.Count != 1) return x => false;
  8.     var p = selector.Parameters.First();
  9.     // Получаем ссылку на метод Contains
  10.     var method = typeof(string).GetMethod("Contains"new[] { typeof(string) });
  11.     // Для каждого значения строим выражение, вызывающее метод String.Contains
  12.     var equals = values
  13.         .Select(v => (Expression)Expression.Call(
  14.             selector.Body, method, Expression.Constant(v, typeof(string))));
  15.     // Объдиняем получившиеся выражения
  16.     var body = equals.Aggregate(Expression.Or);
  17.     // Возвращаям получившиеся выражение
  18.     return Expression.Lambda<Func<T, bool>>(body, p);
  19. }

Исходный коды демонстрационного проекта можно скачать здесь.

Надеюсь, эта серия постов об использовании Linq to SharePoint поможет тем, кто использует его на проектах и убедит использовать его тех, кто ещё этого не делает.


Поделиться

Коментарии