Чтобы упростить жизнь разработчику клиентских компонентов я написал REST веб-сервис для SharePoint 2010.
Реализация
За основу взял метод, который описывал ранее - Javascript-enabled SharePoint WCF services.
Контракт у сервиса очень простой:
[ServiceContract] public interface ISearch { [OperationContract] [WebGet(BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] Stream Query(string q, int top, int skip, string select, string orderBy, bool includeRefiners, string refiners); }
Параметры вызова:
- q – текст запроса (обязательно).
- top – количество результатов.
- skip – с какой позиции в выборке отдавать результаты.
- select – через запятую названия managed properties в результатах.
- orderBy – через запятую названия managed properties по которым сортировать результат, после имени можно указать desc для сортировки по убыванию.
- includeRefiners – true или false, возвращать результаты уточнений или нет.
- refiners - через запятую названия managed properties для формирования уточнений.
Реализация:
public System.IO.Stream Query(string q, int top, int skip, string select, string orderBy, bool includeRefiners, string r) { using (new SPMonitoredScope("Execute Query Method")) { var site = SPContext.Current.Site; var result = GetSearchResults(site, q, top, skip, select, orderBy, includeRefiners, r); return ToJson(result); } }
Метод GetSearchResults довольно простой, он передает параметры запроса в объект KeywordQuery и получает результат.
private static ResultTableCollection GetSearchResults(SPSite site, string q, int top, int skip, string select, string orderBy, bool includeRefiners, string r) { var query = new KeywordQuery(site); query.QueryText = q; query.StartRow = skip; if (top > 0) { query.RowLimit = top; } FillSelectProperties(select, query); FillSortList(orderBy, query); query.ResultTypes = ResultType.RelevantResults; if (includeRefiners) { query.ResultTypes |= ResultType.RefinementResults; query.Refiners = r; } return query.Execute(); }
Методы FillSelectProperties и FillSortList парсят значения из строки запроса и заполняют свойства объекта KeywordQuery.
private static void FillSortList(string orderBy, KeywordQuery query) { if (!string.IsNullOrEmpty(orderBy)) { var orderByParts = orderBy.Split(new[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries); query.SortList.Clear(); foreach (var part in orderByParts) { var pair = part.Split(' '); if (pair.Length > 1 && string.Compare(pair[1], "desc", System.StringComparison.OrdinalIgnoreCase) == 0) { query.SortList.Add(pair[0], SortDirection.Descending); } else { query.SortList.Add(pair[0], SortDirection.Ascending); } } } } private static void FillSelectProperties(string select, KeywordQuery query) { if (!string.IsNullOrEmpty(select)) { var properties = select.Split(new[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries); query.SelectProperties.Clear(); query.SelectProperties.AddRange(properties); } }
Теперь самая интересная часть – преобразование результатов в JSON. Для сериализации не подойдет стандартный DataContractJsonSerializer, он не умеет сериализовывать DataSet и DataTable в компактном виде. Со времен появления ASP.NET Ajax в библиотеке появился класс JavaScriptSerializer. Он не очень быстр, зато его легко расширять, чтобы получать ровно ту разметку, которая нужна и не требуется дополнительных библиотек.
Метод ToJson:
private static Stream ToJson(ResultTableCollection value) { JavaScriptSerializer ser = new JavaScriptSerializer(); List<JavaScriptConverter> converters = new List<JavaScriptConverter>(); converters.Add(new DataRowConverter()); converters.Add(new ResultTableCollectionConverter()); ser.RegisterConverters(converters); var resultStream = new MemoryStream(); var writer = new StreamWriter(resultStream); writer.Write(ser.Serialize(value)); writer.Flush(); resultStream.Position = 0; return resultStream; }
Для сериализации используется два дополнительных конвертера.
ResultTableCollectionConverter:internal class ResultTableCollectionConverter : JavaScriptConverter { public override IEnumerable<Type> SupportedTypes { get { return new Type[] { typeof(ResultTableCollection) }; } } public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer) { throw new NotImplementedException(); } public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer) { var resultTableCollection = obj as ResultTableCollection; Dictionary<string, object> propValues = new Dictionary<string, object>(); if (resultTableCollection != null) { if (resultTableCollection.Exists(ResultType.RelevantResults)) { var resultTable = resultTableCollection[ResultType.RelevantResults]; propValues.Add("TotalResults", resultTable.TotalRows); propValues.Add("Results", resultTable.Table.Rows.OfType<DataRow>()); } if (resultTableCollection.Exists(ResultType.RefinementResults)) { var refinersTable = resultTableCollection[ResultType.RefinementResults]; propValues.Add("TotalRefiners", refinersTable.TotalRows); propValues.Add("Refiners", refinersTable.Table.Rows.OfType<DataRow>()); } } return propValues; } }
DataRowConverter:
internal class DataRowConverter : JavaScriptConverter { public override IEnumerable<Type> SupportedTypes { get { return new Type[] { typeof(DataRow) }; } } public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer) { throw new NotImplementedException(); } public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer) { DataRow dataRow = obj as DataRow; return dataRow != null ? dataRow.Table.Columns.OfType<DataColumn>().ToDictionary(c => c.ColumnName, c => dataRow[c]) : new Dictionary<string, object>(); } }
Применение
Возможность делать поисковые запросы на клиенте позволяет создавать чисто клиентские веб-части, которые не требуют для работы серверного кода. Для реализации этой идеи я реализовал одну базовую веб-часть, которая работает на jQuery и jsRender, и позволяет задавать параметры и настройки на уровне .webpart файла. Таким образом одни раз установив Farm Solution с веб-сервисом и базовой веб-частью появляется возможность добавлять веб-части с клиентским кодам в виде Sandbox решений.
Пример такого решения я, как обычно, выложил на codeplex:
Исходники - https://spsamples.codeplex.com/SourceControl/latest#SearchWidgetWebParts/
Релиз - https://spsamples.codeplex.com/releases/view/118068
Кстати это решение, в слегка измененном виде, уже больше года работает на крупном портале.
Заключение
Это последняя часть серии про использование поиска в SharePoint 2010. Часть 1 и Часть 2 по ссылкам.