Ранее я приводил список задач для проверки навыков программирования для SharePoint. Сегодня напишу о решении второй задачи про соединенные веб-части.

Задача

  1. Создать веб-часть дерева организаций (подразделений)
  2. Сделать её провайдером IWebPartTable
  3. При выборе узла дерева веб-часть должна отправлять   профили пользователей в организации
  4. Создать веб-часть потребитель IWebPartTable с помощью SPGridView

Подготовка

Разработка SharePoint полна тонких моментов, поэтому критически необходимо внимательно читать документацию на MSDN и то что пишут в блогах до написания кода.

Для решения задачи с соединенными веб частями понадобятся следующие ссылки:

  1. Жизненный цикл веб-частей в SharePoint. Не уверен что сведения в этой статье точные, но они достаточно полные (помните правило: если о чем-то пишут, то не значит что оно так и работает).
  2. Описание IWebPartTable. В конце статьи есть пример реализации, все довольно просто.
  3. Описание SPGridView: часть 1, часть 2, часть 3, часть 4. Очень хорошая серия если будете использовать SPGridView в своих проектах.
  4. Классы OrganizationProfileManager и UserProfileManager для работы с профилями.

Создание проекта

Выбираем Empty SharePoint Project, создаем его как Farm Solution

image

Добавляем две веб-части

image

По-умолчанию Visual Studio 2010 прописывает в .webpart файлах имена классов. это крайне неудобно, так как необходимо вручную править .webpart файлы при переименовании классов\изменении пространства имен.

Используя сведения отсюда, добавляем атрибуты классам веб-частей и изменяем .webpart файлы.

image

image

Создание дерева организаций

Для вывода дерева будем использовать контрол TreeView.

private TreeView tree;

protected override void CreateChildControls()
{
    this.tree = new TreeView();
    Controls.Add(this.tree);
}

Заполнение дерева организаций делается так:

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    EnsureChildControls();
    var opm = new OrganizationProfileManager(SPServiceContext.Current);
    this.tree.Nodes.Clear();
    AddChildNodesRecursive(tree.Nodes, opm.RootOrganization);
}

private void AddChildNodesRecursive(TreeNodeCollection treeNodeCollection, 
                                    OrganizationProfile organizationProfile)
{
    var node = new TreeNode(organizationProfile.DisplayName, 
                            organizationProfile.RecordId.ToString());
    foreach (var child in organizationProfile.GetChildren())
    {
        AddChildNodesRecursive(node.ChildNodes, child);
    }

    treeNodeCollection.Add(node);
}

Те кто много писал кода для asp.net webforms сразу пытаются написать в методе OnLoad что-то вроде

if(!this.Page.IsPostBack)
{
    //....
}
Такой код верный если контрол находится в разметке страницы и его метод OnLoad выполняется при первой загрузке страницы. Для веб-частей в общем случае это неверно. Веб-части могут быть добавлены на страницу после её первой загрузки и код внутри if не выполнится. В таких случаях рекомендую анализировать состояние контролов и выполнять загрузку по необходимости. Но об этом в следующих постах.

Провайдер IWebPartTable

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

[ConnectionProvider("Users")]
public IWebPartTable SendUsersFromSelectedNode()
{
    return this;
}

Название метода не имеет значения. Текст указанный в атрибуте используется в UI для формирования пункта меню, если создаете многоязычное приложение, то необходимо позаботиться о локализации. Сопоставление провайдера и потребителя осуществляется только по типу интерфейса.

Почти всегда методы провайдеров возвращают this, а необходимый интерфейс реализуется веб-частью. Таким образом методы интерфейса могут обращаться к состоянию веб-части для получения значений.

Теперь необходимо при запросе данных потребителем формировать некоторый объект, точнее коллекцию объектов (обычно используют DataTable) и схему этих данных в виде PropertyDescriptorCollection. Тут очень важны два момента:

  1. Формировать данные при запросе потребителем, а не при изменении состояния веб-части. Многие, кто выполнял данную задачу, пытались перехватывать событие дерева об изменении выбранного узла, формировали DataTable и записывали в поле класса. Естественно после PostBack значение терялось. А если попытаться сохранить эти данные во ViewState, то начнет пухнуть страница.
  2. Формировать данные только тогда, когда потребитель запросит их, потому что веб-части могут быть не соединены и работа будет делаться впустую.
private DataTable usersTable;

public void GetTableData(TableCallback callback)
{
    if (callback != null)
    {
        EnsureUsersTable();
        if (this.usersTable != null)
        {
            callback(this.usersTable.DefaultView);
        }
    }
}

public PropertyDescriptorCollection Schema
{
    get 
    {
        EnsureUsersTable();
        if (this.usersTable != null && this.usersTable.Rows.Count > 0)
        {                    
            return TypeDescriptor.GetProperties(this.usersTable.DefaultView[0]);
        }
        return null;
    }
}

Интефейс IWebPartTable не говорит о том в каком порядке будут вызываться члены Schema и GetTableData, поэтому для устойчивости решения необходимо поддерживать любой сценарий.

Сама загрузка данных происходит в методе EnsureUsersTable:

private void EnsureUsersTable()
{
    EnsureChildControls();
    if (this.usersTable == null)
    {
        if (this.tree.SelectedNode != null)
        {
            long recordId = 0;

            if (long.TryParse(this.tree.SelectedValue, out recordId))
            {
                this.usersTable = LoadOrganizationMembers(recordId);
            }
        }
    }
}

Приведенный выше код реализует паттерн Ensure\Create, который часто используется в asp.net webforms. Жизненный цикл страницы и контролов довольно сложен и может изменяться в зависимости от различных условий. Некоторые события могут не выполняться вообще, и поэтому необходимо удостовериться что данные загружены (Ensure).

Сам код LoadOrganizationMembers довольно очевидный. Первая часть метода формирует колонки DataTable, вторая часть метода заполняет строки DataTable.

private DataTable LoadOrganizationMembers(long recordId)
{
    var opm = new OrganizationProfileManager(SPServiceContext.Current);
    var org = opm.GetOrganizationProfile(recordId);

    var upm = new UserProfileManager(SPServiceContext.Current);

    //Filter section headers from property list
    var props = from prop in upm.DefaultProfileSubtypeProperties
                where !prop.IsSection
                orderby prop.DisplayOrder
                select prop;

    var columns = from p in props
                  select new DataColumn
                  {
                      ColumnName = p.Name,
                      Caption = p.DisplayName                              
                  };

    var result = new DataTable();
    result.Columns.AddRange(columns.ToArray());

    //Load profile properties to table
    foreach (var member in org.GetImmediateMembers())
    {
        var row = result.NewRow();
        foreach (var p in props)
        {
            row[p.Name] = Convert.ToString(member[p.Name].Value);
        }
        result.Rows.Add(row);
    }
    return result;
}

Обратите внимание на использование Convert, класс незаслуженно забытый программистами на C#. В SharePoint часто туда-сюда передаются object, которые могут быть внутри любыми типами. Наиболее устойчивый способ преобразовать к нужному типу – с использованием класса Convert. Данный класс выполняет приведение типов или парсинг в зависимости от типа входного значения, кроме того он наиболее корректно учитывает null.

Создание веб-части потребителя

Для создания веб-части потребителя требуется определить один метод и пометить его атрибутом

private IWebPartTable provider;

[ConnectionConsumer("Table")]
public void GetTableProvider(IWebPartTable provider)
{
    this.provider = provider;
}
Как и в случае с провайдером название метода не имеет значения, а текст в атрибуте используется в UI. Сопоставление производится по типу аргумента.

Далее создание контролов

protected override void CreateChildControls()
{
    this.grid = new SPGridView();
    grid.AutoGenerateColumns = false;
    this.Controls.Add(grid);
}

Как написано в статье, хорошее место для вызова методов получения данных от провайдера метод OnPreRender

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);
    this.provider.GetTableData(d =>
    {
        if (d != null && d.Count > 0)
        {
            this.grid.DataSource = d;
            GenerateColumns(this.grid, this.provider.Schema);
            this.grid.DataBind();
        }
    });
}

Тут важно помнить что callback, переданный в GetTableData может быть не вызван вообще. Поэтому все действия по генерации колонок в гриде и связыванию с данными выполняются внутри callback.

Сам метод генерации колонок

private void GenerateColumns(SPGridView grid, PropertyDescriptorCollection schema)
{
    grid.Columns.Clear();
    if (schema != null)
    {
        foreach (PropertyDescriptor property in schema)
        {
            grid.Columns.Add(
                new SPBoundField 
                { 
                    DataField = property.Name, 
                    HeaderText = property.DisplayName, 
                    SortExpression = property.Name 
                });
        }
    }
}

Этого достаточно для вывода статической таблицы.

Суммарно весь код занимает не более 100 строк с точки зрения Visual Studio. Его вполне можно написать за час.

Следующий раз расскажу про то как оптимизировать передачу данных о профилях, как добавить сортировку с фильтрацией в таблицу, о Bin deployment и других интересных вещах, а также где взять исходники этого проекта.

Теги : ASP.NET, web parts, SharePoint