Ранее я приводил список задач для проверки навыков программирования для SharePoint. Сегодня напишу о решении второй задачи про соединенные веб-части.
Задача
- Создать веб-часть дерева организаций (подразделений)
- Сделать её провайдером IWebPartTable
- При выборе узла дерева веб-часть должна отправлять профили пользователей в организации
- Создать веб-часть потребитель IWebPartTable с помощью SPGridView
Подготовка
Разработка SharePoint полна тонких моментов, поэтому критически необходимо внимательно читать документацию на MSDN и то что пишут в блогах до написания кода.
Для решения задачи с соединенными веб частями понадобятся следующие ссылки:
- Жизненный цикл веб-частей в SharePoint. Не уверен что сведения в этой статье точные, но они достаточно полные (помните правило: если о чем-то пишут, то не значит что оно так и работает).
- Описание IWebPartTable. В конце статьи есть пример реализации, все довольно просто.
- Описание SPGridView: часть 1, часть 2, часть 3, часть 4. Очень хорошая серия если будете использовать SPGridView в своих проектах.
- Классы OrganizationProfileManager и UserProfileManager для работы с профилями.
Создание проекта
Выбираем Empty SharePoint Project, создаем его как Farm Solution
Добавляем две веб-части
По-умолчанию Visual Studio 2010 прописывает в .webpart файлах имена классов. это крайне неудобно, так как необходимо вручную править .webpart файлы при переименовании классов\изменении пространства имен.
Используя сведения отсюда, добавляем атрибуты классам веб-частей и изменяем .webpart файлы.
Создание дерева организаций
Для вывода дерева будем использовать контрол 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. Тут очень важны два момента:
- Формировать данные при запросе потребителем, а не при изменении состояния веб-части. Многие, кто выполнял данную задачу, пытались перехватывать событие дерева об изменении выбранного узла, формировали DataTable и записывали в поле класса. Естественно после PostBack значение терялось. А если попытаться сохранить эти данные во ViewState, то начнет пухнуть страница.
- Формировать данные только тогда, когда потребитель запросит их, потому что веб-части могут быть не соединены и работа будет делаться впустую.
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 и других интересных вещах, а также где взять исходники этого проекта.
