Ранее я приводил список задач для проверки навыков программирования для 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 и других интересных вещах, а также где взять исходники этого проекта.