Создание пользовательского фильтра для списка в SharePoint 2010

в 14:20, , рубрики: development, Sharepoint2010, разработка, метки: , ,

В данной статье будут последовательно описаны все шаги, необходимые для написания универсального фильтра, позволяющего производить отбор из стандартных списков SharePoint (наследников от XsltListWebPart или ListWebPart).

Разработка велась на VisualStudio 2010, которая умеет работать только с SharePoint 2010, поэтому всё описанное ниже проверялось на платформе SharePoint Foundation 2010, однако, скорее всего, всё будет справедливо и для SharePoint 2007 (WSS 3.0).

Процесс установки и развёртывания среды разработки описывать не буду – это легко ищется в интернете, а вот как сделать фильтрацию стандартных списков – тут хороших и понятных примеров я не нашёл.

Итак, задача-минимум: Создать WebPart, который мог бы фильтровать список Shared Documents примерно вот в таком виде:

image

Т.е. можно выбрать одно из доступных полей списка (не только из числа видимых), указать один из возможных операторов сравнения (равно, не равно, больше, меньше и т.д) и собственно значение. При нажатии на кнопку Go – содержимое списка должно удовлетворять нашему условию.

Рассмотрим для начала как вообще в SharePoint’е WebPart’ы могут взаимодействовать друг с другом. Стандартным методом взаимодействия двух или более WebPart’ов является так называемый механизм ProviderConsumer.

В начале определяется некий интерфейс, через который будет осуществляться связь.

Пример интерфейса

public interface ITextBoxString
  {
    string TextBoxString { get; set; }
  }

Consumer публикует так называемые точки входа (фактически это методы принимающие в качестве параметра какой-либо интерфейс и помеченный атрибутом ConnectionConsumer).

Пример Consumer

  public class StringConsumer : WebPart
  {
    private ITextBoxString _myProvider;
    private Label _myLabel;
  
    protected override void OnPreRender(EventArgs e)
    {
      EnsureChildControls();
      if (_myProvider != null)
        _myLabel.Text = _myProvider.TextBoxString;
    }
  
    protected override void CreateChildControls()
    {
      Controls.Clear();
      _myLabel = new Label{Text = "Default text"};
      Controls.Add(_myLabel);
    }
  
    [ConnectionConsumer("String Consumer", "StringConsumer")]
    public void TextBoxStringConsumer(ITextBoxString provider)
    {
      _myProvider = provider;
    }
  }

Provider публикует исходящие точки (методы, возвращающие интерфейс и помеченные атрибутом ConnectionProvider).

Пример Provider

  public class StringProvider : WebPart, ITextBoxString
  {
    private TextBox _myTextBox;
  
    [Personalizable]
    public string TextBoxString { get { return _myTextBox.Text; } set { _myTextBox.Text = value; } }
  
    protected override void CreateChildControls()
    {
      Controls.Clear();
      _myTextBox = new TextBox();
      Controls.Add(_myTextBox);
      Controls.Add(new Button {Text = "Change Text"});
    }
  
    [ConnectionProvider("Provider for String From TextBox", "TextBoxStringProvider")]
    public ITextBoxString TextBoxStringProvider()
    {
      return this;
    }
  }

Связь может быть установлена если интерфейсы у точек связи Provider и Consumer совпадают. При этом после размещения соответствующих WebPart’ов на странице их можно будет связать между собой, выбрав в пункте меню Connection одного из WebPart’ов.  После того как связь установлена – после каждого обновления (рендеринга) страницы сначала будет вызван метод Provider’а (TextBoxStringProvider) для получения интерфейса и результат будет передан в метод Consumer’а (TextBoxStringConsumer). В принципе всё просто. Этот академический пример даже описан в MSDN.

Однако нам нужно написать собственный провайдер к предопределённому Consumer’у, а именно WebPart’ам, наследникам от XsltListWebPart или ListWebPart. Для этого нам нужно знать какие интерфейсы связи они поддерживают и как с ними можно работать. Этой информации, как ни странно, в интернете крайне мало. Как в принципе мало и решений, решающих данную задачу (я нашёл два коммерческих проекта (KWizCom List Filter Plus и Roxority FilterZen Filter) и ни одного с открытым исходным кодом).

Кропотливый анализ и реверс-инжиниринг показал, что существует два интерфейса к которым можно подключиться: ITransformableFilterValues и IWebPartParameters.

При помощи первого интерфейса ITransformableFilterValues можно произвести отбор только по одному полю списка (возможно и по нескольким, но как, я навскидку не нашёл), выбранному при установки связи между нашим провайдером и списком и только по полному соответствию значения поля с фильтруемым значением (т.е. можно реализовать только операцию “равно”). Таким образом этот путь нам не подходит.

Второй интерфейс IWebPartParameters интересен тем, что во-первых, позволяет легко получить список всех полей списка:

IWebPartParameters

  public void SetConsumerSchema(PropertyDescriptorCollection schema)
  {
    Owner.Parameters = schema;
  }

А во-вторых, не требует указания, по какому/каким полям осуществляется фильтрация. В методе GetParametersData передаётся словарь с парами имя поля – значение для осуществления отбора:

IWebPartParameters

  public void GetParametersData(ParametersCallback callback)
  {
    var objParameters = new StateBag();
    if (Owner._searchBox != null && !string.IsNullOrEmpty(Owner._searchBox.Text))
    {
      objParameters.Add(Owner._comboBox.SelectedItem.Value, Owner._searchBox.Text);
    }
    callback(objParameters);
  }

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

Что же делать? Оказывается повлиять на другой WebPart можно и другим способом – просто изменив в подходящий момент времени (перед отбором) параметры этого WebPart’а.

Подходящим моментом времени будет событие OnLoad – которое вызывается после загрузки страницы, но до отбора данных. Изменяемым же параметром будет Xml-описание списка, которое хранится в свойстве XmlDefinition для XlstListWebPart и ListViewXml для ListWebPart. Данное Xml-описание помимо всего прочего имеет CAML-запрос, для отбора данных в который можно вставить необходимые нам условия:

Code Sample

  private string CreateQuery()
  {
    if (_searchBox != null && !string.IsNullOrEmpty(_searchBox.Text))
      return String.Format("<{1}><FieldRef Name='{0}'/><Value Type='Text'>{2}</Value></{1}>",
        _comboBox.SelectedItem.Value, _searchType.SelectedItem.Value, _searchBox.Text);
    else
      return "";
  }
  
  protected override void OnLoad(EventArgs e)
  {
    base.OnLoad(e);
  
    var query = CreateQuery();
    if (query == "") return;
    var part = WebPartManager.WebParts[0] as XsltListViewWebPart;
    if (part == null) return;
  
    var doc = new XmlDocument();
    doc.LoadXml(part.XmlDefinition);
    var queryNode = doc.SelectSingleNode("//Query");
    if (queryNode == null) return;
  
    var whereNode = queryNode.SelectSingleNode("Where");
    if (whereNode != null) queryNode.RemoveChild(whereNode);
  
    var newNode = doc.CreateNode(XmlNodeType.Element, "Where", String.Empty);
    newNode.InnerXml = query;
    queryNode.AppendChild(newNode);
    part.XmlDefinition = doc.OuterXml;
  }

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

Автор: LionSoft

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js