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.
Environment
Все примеры будут основываться на модели из 2 части постов, посвященных Linq to SharePoint. Для демонстрации в этом посте я добавил два поля в тип содержимого Employee. Первое поле - Sex (SPFieldChoice), второе - Hobbies (SPFieldMultiChoice):
- <Field ID="{68c1ee4a-5a25-4ccb-82ca-f5ff17e2016f}" Name="Sex" DisplayName="Sex" Type="Choice" Format="RadioButtons">
- <CHOICES>
- <CHOICE>Male</CHOICE>
- <CHOICE>Female</CHOICE>
- </CHOICES>
- <Default>Male</Default>
- </Field>
- <Field ID="{BD1898B4-0869-41F2-9DB0-B0F1B8F139D3}" Name="Hobbies" DisplayName="Hobbies" Type="MultiChoice" Mult="TRUE">
- <CHOICES>
- <CHOICE>Chess</CHOICE>
- <CHOICE>Football</CHOICE>
- <CHOICE>Basketball</CHOICE>
- </CHOICES>
- <Default>Chess</Default>
- </Field>
Теперь, развернув решения и создав код с помощью SPMetal, мы получим для этих полей два перечисления. Один из которых с атрибутом FlagsAttribute (для множественных значений):
- public enum EmployeeSex
- {
- None = 0,
- Invalid = 1,
- [Choice(Value = "Male")]
- Male = 2,
- [Choice(Value = "Female")]
- Female = 4
- }
-
- [FlagsAttribute]
- public enum EmployeeHobby
- {
- None = 0,
- Invalid = 1,
- [Choice(Value = "Chess")]
- Chess = 2,
- [Choice(Value = "Football")]
- Football = 4,
- [Choice(Value = "Basketball")]
- Basketball = 8
- }
Single Choice
Начнем с простого случая, когда у нас поле может иметь только одно значение. Имея код, созданный SPMetal, все кажется очень просто. Если нам необходимо выбрать всех сотрудников мужского пола, то логично было бы использовать следующий код:
- using (var ctx = new ZhukDataContext(siteUrl))
- {
- var employees = ctx.Employees
- .Where(emp => emp.Sex == EmployeeSex.Male)
- .ToList();
- // ...
- }
Но в этом случае SPLinqProvider не сможет конвертировать этот запрос в ожидаемый нами CAML. Сначала произойдет выгрузка всех элементов, а уже потом в памяти произойдет фильтрация. Связано это с тем, что поля типа Choice и MultiChoice хранятся в базе данных в виде текстовых значений.
Чтобы обойти это ограничение я использую следующий трюк: я изменяю тип свойства с перечисления на текст. А, чтобы сохранить использование перечислений, можно использовать поле без атрибута ColumnAttribute:
- public EmployeeSex? Sex
- {
- get
- {
- var res = Enum.Parse(typeof (EmployeeSex), _sexValue);
- return res is EmployeeSex ? (EmployeeSex) res : EmployeeSex.Invalid;
- }
- }
-
- [Column(Name = "Sex", Storage = "_sexValue", FieldType = "Choice")]
- public string SexValue
- {
- get
- {
- return _sexValue;
- }
- set
- {
- if ((value == _sexValue)) return;
- var vals = Enum.GetValues(typeof(EmployeeSex));
- foreach (EmployeeSex val in vals)
- {
- if (!string.Equals(Enum.GetName(typeof(EmployeeSex), val), value,
- StringComparison.InvariantCultureIgnoreCase)) continue;
- OnPropertyChanging("SexValue", _sexValue);
- _sexValue = value;
- OnPropertyChanged("SexValue");
- }
- }
- }
Теперь фильтровать можно по текстовому полю SexValue, получая в результате правильный CAML-запрос. А чтобы избежать использование текстовых значений в коде, можно написать метод расширитель для перечислений, который будет возвращать значение свойства Value атрибута ChoiceAttribute:
- public static string GetChoiceValue(this Enum enumerator)
- {
- // Получаем тип
- var type = enumerator.GetType();
- // Получаем имя поля
- var fieldName = Enum.GetName(enumerator.GetType(), enumerator);
- // Получаем поле
- var field = type.GetField(fieldName,
- BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public);
- // Получаем атрибуты найденного поля
- var attributes = field.GetCustomAttributes(typeof (ChoiceAttribute), true);
- var attribute = attributes.FirstOrDefault();
- // Если атрибута нет, то возвращаем пустую строку
- // Иначе возвращаем значение Value атрибута
- return attribute == null
- ? string.Empty
- : ((ChoiceAttribute) attribute).Value;
- }
Вот теперь все работает правильно и выглядит довольно красиво:
- using (var ctx = new ZhukDataContext(siteUrl))
- {
- var employees = ctx.Employees
- .Where(emp => emp.SexValue == EmployeeSex.Male.GetChoiceValue())
- .ToList();
- }
Сгенерированный в обоих случаях CAML-запрос я здесь приводить не буду - очень много места он занимает. Все это можно будет попробовать, взяв демонстрационный проект.
Multiple Choice
А вот в этом случае ничего уже не выйдет (по крайней мере, я не придумал, как это обойти). Убежден, что копать надо в сторону того, что все значения полей типа MultiChoice в базе данных обрамлены с обеих сторон разделителем (;#):
Остается только привести значение перечислителя к тексту. И использовать аналог метода расширителя EqualsAny, описанный мною в 4 части. Только наш новый метод будет проверять на содержание значения в строке:
- public static Expression<Func<T, bool>> ContainsAny<T>(this Expression<Func<T, string>> selector,
- IEnumerable<string> values)
- {
- // Если значений нет, то возвращаем выражение x=> false
- if (!values.Any()) return x => false;
- // Аналогично поступаем в случае, когда кол-во параметров не равно одному
- if (selector.Parameters.Count != 1) return x => false;
- var p = selector.Parameters.First();
- // Получаем ссылку на метод Contains
- var method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
- // Для каждого значения строим выражение, вызывающее метод String.Contains
- var equals = values
- .Select(v => (Expression)Expression.Call(
- selector.Body, method, Expression.Constant(v, typeof(string))));
- // Объдиняем получившиеся выражения
- var body = equals.Aggregate(Expression.Or);
- // Возвращаям получившиеся выражение
- return Expression.Lambda<Func<T, bool>>(body, p);
- }
Исходный коды демонстрационного проекта можно скачать здесь.
Надеюсь, эта серия постов об использовании Linq to SharePoint поможет тем, кто использует его на проектах и убедит использовать его тех, кто ещё этого не делает.