Статьи про IoC, AOP и контейнер Unity
Первые посты про IoC я написал более двух лет назад, но судя по статистике блога люди часто к ним обращаются.
В этом после приведу индекс статей в порядке чтения для облегчения поиска и навигации
- Введение в IoC
- Основная задача IoC-контейнеров
- Первые шаги с Unity
- Рефакторинг legacy-кода для использования Unity
- Использование generic-классов с Unity
- Инъекция массивов в Unity
- Unity LifetimeManager
- Конфигурация Unity
- Инъекция самого контейнера Unity в объекты
- AOP в Unity
- Фабрики в Unity
- Практика AOP: аудит изменений данных в EF с учетом котнекста операций
- Unity 2.0
- AOP в Unity 2.0
SOLID
Эта аббревиатура является самой известной (после ООП), она говорит нам о 5 принципах “хорошего дизайна ПО”. При этом является самой бесполезной, потому что однозначно никто не может обозначить критерий для того или иного принципа. Часто на форумах приходится видеть споры о том у кого программа SOLIDнее.
Про SOLID пишут часто и много, но большинство пишущих не читали или мало читали первоисточник (признайтесь, вы читали?). Автор аббревиатуры SOLID - Роберт Мартин, он придумал саму аббревиатуру и описал 5 принципов. На самом деле он описал больше, но звучных буквосочетаний не придумал, многие вещи остались забытыми. Заметьте что Мартин именно описал принципы, он не является их автором. Зачастую объяснения на пальцах на примерах сложно перенести в свой код.
Who is mister SOLID?
Аббревиатура (длинное и неприятное слово) SOLID состоит из:
- Single Responsibility Principle (SRP) – принцип единственной отвественности
- Open\Close Principle (OCP) – принцип открытости\закрытости
- Liskov Substitution Principle (LSP) – принцип подстановки Лисков (это фамилия)
- Interface Segregation Principle (ISP) – принцип изоляции интерфейсов
- Dependency Inversion Principle (DIP) – принцип инверсии зависимостей
Далее буду пользоваться только акронимами, указанными в скобках.
Критика
Как связаны между собой вышеуказанные принципы никто не говорит, какой из них важнее, а какой нет – тоже никто не в курсе.
Разберем по отдельности все 5 принципов, для описания буду брать из википедии. Вероятнее всего именно это описание найдет человек.
SRP
На каждый объект должна быть возложена одна единственная обязанность.
Первое же определение взрывает мозг. Что такое обязанность? Мартин определяет обязанность как причину изменения. Стало понятнее? Мне не очень.
OCP
Программные сущности должны быть открыты для расширения, но закрыты для изменения.
Это как вообще? Открыты для расширения – еще куда ни шло, а что значит закрыты для изменения? Скомпилированный код и так поменять нельзя, а если правятся исходники, то какая разница?
LSP
Объекты в программе могут быть заменены их наследниками без изменения свойств программы.
Тут немного лучше, потому что принцип LSP предельно формален, про него подробнее напишу ниже.
ISP
Много специализированных интерфейсов лучше, чем один универсальный.
Это Мартин решил поиграть в Капитана Очевидность.
DIP
Зависимости внутри системы строятся на основе абстракций. Модули верхнего уровня не зависят от модулей нижнего уровня. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Это просто бред, написанный для красного словца. Если все зависит от абстракций, то как понять где модули верхнего уровня, а где нижнего?
У Мартина, кстати, не такое определение.
Терминология
Чтобы навести формализм во всей этой кухне необходимо ввести определения. большинство горячих споров происходят как раз из-за разных определений.
Итак определения
Интерфейс – некоторый набор функций (их параметров и возвращаемых значений), с помощью которого одна часть программы обращается к другой.
Контракт – надмножество интерфейса, описывающее также поведение функций, ограничения на входные\выходные параметры, инварианты, последовательность вызовов итд. Контракт обычно присутствует в программе неявно, но есть средства, позволяющие часть его описать явно. Например навороченные системы типов как в haskell, внешние средства вроде Code Contracts в .NET. Даже если контракт не определен явно, то в программе он неявно присутствует.
Абстрактный интерфейс – некоторый тип данных,состоящий из набора методов без реализации. Всегда соответствует некоторому интерфейсу.
Класс – некоторый тип данных, который состоит из данных и методов. Классы могут наследоваться один от другого, могут реализовывать несколько абстрактных интерфейсов. Класс всегда имеет некоторый интерфейс и контракт, зачастую больше одного.
Модуль – некоторое множество функций и, возможно, данных, объединенные для решения задачи. Модулем могут быть как классы в ОО-языках, так и другие средства группирования кода. Модуль также имеет интерфейс и контракт, скорее всего один.
Принципы
Для начала стоит сказать что многие принципы не являются прерогативой ООП, а применимы для широкого класса парадигм.
Начнем с SRP
Если часть некоторого модуля не имеет никаких ссылок на другую часть этого модуля, то эти части можно разделить на разные модули. Если модули могут меняться независимо, то разделить нужно.
Начинать стоит с простого: если можно отделить – надо отделить. Необходимо чтобы внутри одного модуля весь функционал был связан между собой (high cohesion, такое словосочетание вы наверное слышали). Принцип работает только в одну сторону: если подмножество A некоторого модуля не имеет ссылок на подмножество B, то это не значит что B не имеет ссылок на A, причем скорее всего именно B будет ссылаться на A.
Следуя данному принципу весь код будет распадаться на множество маленьких модулей, многие из которых выродятся до одной функции. Это нормально, даже хорошо. Функции потом можно группировать в модули по логической связности, добиваясь все того же high cohesion.
Модули будут зависеть друг от друга, они будут выстраиваться в ориентированный граф. Расположив зависимости сверху вниз можно условно разделить модули на верхне- и нижне- уровневые. На самом “пространство”, в котором мы пытаемся упорядочить модули, многомерно. Придумать одно отношение порядка для всего этого пространства невозможно. Но для двух модулей, между которыми есть путь, можно сказать какой из них верхнеуровневый, а какой нижнеуровневый.
Зависимость между модулями может быть:
- Ссылочной, когда модуль A непосредственно обращается к модулю B, его функциям и данным.
- Наследованием, когда модуль A является частным случаем B.
- Зависимостью по состоянию, когда два модуля оперируют одним внешним состоянием (глобальные переменные, файлы, БД) и влияют на работу друг друга. Это плохая зависимость, от нее надо избавляться.
- Зависимостью по времени. Когда для работы требуется одного модуля требуется вызов функций другого модуля в нужные моменты. Это самый плохой вид зависимости, он него надо избавляться однозначно всеми возможными способами.
Далее ISP
Как я уже писал выше, если следовать SRP, то программа распадется на множество мелких модулей. ISP говорит нам что это хорошо. Далее когда занимаетесь объединением отдельных функций в некоторые модули по смысловой связности, то учитывайте также ISP, не создавая модулей с очень жирным интерфейсом.
Перейдем к DIP
Этот принцип говорит нам что для двух ссылочно связанных модулей надо создавать абстрактный интерфейс. Оформлять модуль, от которого зависят другие модули, в виде класса, реализующего данный абстрактный интерфейс. Зависимый модуль должен обращаться к абстрактному интерфейсу, а не к конкретному классу. (low coupling, тоже слышали)
Есть проблема в том что надо создавать экземпляры классов, чтобы потом их передавать в зависимые модули. Эту проблему решают IoC-контейнеры, о которых я писал ранее.
Теперь рассмотрим LSP
Принцип подстановки Барбары Лисков сформулирован предельно формально и говорит вообще о любых типа, а не только классах ООП.
Если A является подтипом B, то в любом месте программы (функции), где требуется объект типа B, можно подставить объект типа A и поведение программы (функции) при этом не изменится.
В ООП если класс A унаследован от класса B, равно как класс A реализует абстрактный интерфейс B, то A является подтипом B, а B является супертипом A. Кроме того некоторые языки программирования поддерживают вариантность типов, для них тоже надо применять LSP, но там помогает компилятор.
Принцип LSP надо использовать максимально широко, надо ориентироваться на весь контракт, в том числе пред- и пост-условия, а также то что не описано в самой программе.
Для контрактов правила простые:
- Предусловия в подтипе должны быть не сильнее, чем в супертипе.
- Постусловия в подтипе должны быть не слабее, чем в супертипе.
- Перечень выбрасываемых исключений в подтипе должен быть не шире, чем в супертипе. (хотя часто на это не обращают внимания)
- Остальные детали контракта, которые нельзя проверить статически, должны проверяться тестами, и подтипы должны проходить все тесты, которые проходят супертипы.
Напоследок OCP
Хорошо понимая LSP легко сообразить о чем говорит OCP.
Все не-sealed классы должны быть спроектированы таким образом, чтобы наследники не могли нарушить LSP.
Ну вот и все.
Что не вошло в пятерку
Принцип бритвы Оккма
Не плодите сущности без нужды
После активного применения SRP у вас будет много маленьких модулей, вплоть до одиночных функций. Тривиальные функции можно непосредственно инлайнить в вызывающий код, простые функции также можно перенести, если они используются только в одном месте. Отдельные функции можно объединять в модули по смысловому назначению, а также по параметрам, задавая их на уровне модуля, а не отдельных функций.
Do not repeat yourself (DRY)
Одинаковый или похожий код должен быть вынесен в отдельный модуль и использован другими. Для этого сильно помогают инструменты вроде IoC-контейнеров с возможностью AOP.
Command-Query Separation (CQS)
Все функции некоторого интерфейса должны быть или некоторыми запросами, возвращающими ответ, или командами, изменяющими состояние системы, но не одновременно. Никогда функция не должна возвращать ответ и изменять состояние системы одновременно.
Пожалуйста не путайте этот принцип с модным нынче CQRS, который является гипертрофированным CQS для непонятно каких целей.
Keep it simple, stupid! (KISS)
Делайте все настолько простыми, насколько можно, но не проще.
Создавайте ровно те модули, которые необходимы для решения задач, если модуль не решает задачу – не создавайте его. Если вы хотите создать модуль, но толком не представляете какую задачу он решает – не создавайте его. Если при переделке программы модули стали ненужным – удалите их. Если в некоторых поведение системы не описано для некоторых входных данных - не делайте частные случаи, опишите явно контракт, который запретит неверные данные.
Заключение
Самыми важными принципами в начале проектирования частей программы являются KISS и SRP. После того как появился некоторый граф модулей надо сразу применять Бритву Оккама, LSP, ISP. Когда начинаете писать код, то применяйте DRY, DIP, CQS и OCP.
Ну вот теперь совсем все.
Решение задач. Соединенные веб-части, продолжение.
В прошлый раз я рассказывал как сделать простые соединенные веб-части. Сегодня буду рассказывать как улучшить решение и сделать его более полезным в реальной жизни.
План действий
- Фильтрация в SPGridView
- Оптимизация передачи данных между веб-частями
- Добавление параметров для веб-частей
- Асинхронная загрузка дерева
- Bin deployment
Фильтрация в SPGridView
Разрабатывая UI на SharePoint мы имеем всю мощь библиотеки контролов ASP.NET, об этом не стоит забывать. Вместо того чтобы реализовывать функционал который уже есть лучше потратить время на изучение существующих возможностей.
Например можно перехватывать события SPGridView для обработки фильтрации, а можно использовать существующий инструментарий контролов Data Source для этих целей.
Подробно описано в статье, здесь приведу вкратце код для достижения цели.
Создание дочерних элементов веб-части:
this.ds = new ObjectDataSource(typeof(GridTableConsumer).AssemblyQualifiedName, "SelectData") { ID = "gridDS", EnableCaching = false }; this.ds.ObjectCreating += (sender, e) => e.ObjectInstance = this; this.Controls.Add(this.ds); this.grid = new SPGridView { ID = "grid", AutoGenerateColumns = false, DataSourceID = this.ds.ID, AllowFiltering = true, FilteredDataSourcePropertyName = "FilterExpression", FilteredDataSourcePropertyFormat = "{1} = '{0}'", }; this.Controls.Add(this.grid);
Для тех кто не знаком с классом ObjectDataSource рекомендую изучить его возможности. Для создания веб-частей очень хорошо подходит.
В данном коде применяется хитрость, чтобы контрол ObjectDataSource не создавал экземпляр класса, в данном случае веб-части, в обработчике события ObjectCreating подсовывается текущий экземпляр.
Далее метод SelectData
Чтобы работали фильтры SPGridView лучше всего возвращать объект DataTable.
IWebPartTable provider; PropertyDescriptorCollection schema; ICollection data; public DataTable SelectData() { EnsureDataAndSchema(); DataTable result = null; if (this.data != null && this.schema != null) { result = this.data.ToDataTable(this.schema); } return result; } private void EnsureDataAndSchema() { if (this.data == null) { this.provider.GetTableData((d, s) => { this.data = d; this.schema = s; }); } }
В данном случае используется простые extension-метод GetTableData и ToDataTable. Их код можно посмотреть в исходниках.
Так как автоматически колонки для SPGridView не создаются, то необходимо это делать в коде, причем на наиболее позднем этапе жизненного цикла.
protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); EnsureDataAndSchema(); GenerateGridColumns(this.grid, this.schema); this.grid.DataBind(); } private static void GenerateGridColumns(SPGridView grid, PropertyDescriptorCollection properties) { grid.Columns.Clear(); if (properties != null) { var fields = properties.OfType<PropertyDescriptor>() .Select(p => new SPBoundField { DataField = p.Name, HeaderText = p.DisplayName, SortExpression = p.Name }) .ToList(); fields.ForEach(grid.Columns.Add); grid.FilterDataFields = string.Join(",", fields.Select(f => f.DataField).ToArray()); } }
Последняя строка в GenerateGridColumns необходима чтобы указать по каким колонкам можно фильтровать.
Ну собственно этого достаточно чтобы работала фильтрация в SPGridView. Все это выглядит как стандартное представление для списка.
Оптимизация
Теперь получается что веб-часть дерева организаций создает из профилей DataTable, передает его веб-части представления в виде таблицы и там еще раз создается DataTable. Можно оптимизировать и создавать таблицу один раз.
Мы можем передавать с помощью интерфейса IWebPartTable сам список сотрудников в выбранном подразделении, а в Schema передавать PropertyDescriptorCollection, которые позволят вытаскивать данные из профилей пользователей.
Для класса UserProfile таких дескрипторов нет, но никто не мешает их написать
public class UserProfilePropertyDescriptor: PropertyDescriptor { public UserProfilePropertyDescriptor(ProfileSubtypeProperty propery) :base(propery.Name, new Attribute[] { new DisplayNameAttribute(propery.DisplayName)}) { } public override bool CanResetValue(object component) { return false; } public override Type ComponentType { get { return typeof(UserProfile); } } public override object GetValue(object component) { return Convert.ToString((component as UserProfile)[this.Name].Value); } public override bool IsReadOnly { get { return false; } } public override Type PropertyType { get { return typeof(string); } } public override void ResetValue(object component) { throw new NotImplementedException(); } public override void SetValue(object component, object value) { (component as UserProfile)[this.Name].Value = value; } public override bool ShouldSerializeValue(object component) { return false; } }
Теперь нужно выкинуть код формирования таблицы, заменив его гораздо более простым
public void GetTableData(TableCallback callback) { if (callback != null) { EnsureChildControls(); if (this.tree.SelectedNode != null) { long recordId = 0; if (long.TryParse(this.tree.SelectedValue, out recordId)) { var profiles = this.OrganizationProfileManager .GetOrganizationProfile(recordId) .GetImmediateMembers(); callback(profiles); } } } }
И в качестве Schema возвращать набор свойств профиля
public PropertyDescriptorCollection Schema { get { if (this.schema == null) { 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 new UserProfilePropertyDescriptor(prop); this.schema = new PropertyDescriptorCollection(props.ToArray()); } return this.schema; } }
Параметры веб-частей
Веб-часть дерева организаций все время возвращает все свойства профиля, в реальном случае такой сценарий бесполезен. Надо научить веб-часть возвращать только нужные свойства.
Для этого надо создать параметр, который будет хранить список свойств.
[WebBrowsable(false)] [Personalizable(PersonalizationScope.Shared)] public string ProfileProperties { get; set; } public HashSet<string> ProfilePropertyNames { get { return new HashSet<string>((ProfileProperties ?? "").Split(new[] { PropertyNamesDelimeter }, StringSplitOptions.None)); } set { ProfileProperties = string.Join(PropertyNamesDelimeter, value.ToArray()); } }
Атрибут WebBrowsable с параметром false говорит что не надо генерировать интерфейс для ввода параметра, а Personalizable говорит что значение этого свойствах надо хранить в свойствах веб-части.
Чтобы редактировать параметр нужен Custom Tool Part, сделать его довольно легко. Надо переопределить всего два метода, один для создания дочерних контролов, как у обычной веб-части, а второй для сохранения настроек.
public class OrganizationTreeToolPart : ToolPart { ListBox list; public OrganizationTreeToolPart() { this.Title = "Profile properties"; } protected OrganizationTree WebPart { get { return this.ParentToolPane.SelectedWebPart as OrganizationTree; } } protected override void CreateChildControls() { this.list = new ListBox() { SelectionMode = ListSelectionMode.Multiple, Height = Unit.Pixel(200) }; var names = this.WebPart.ProfilePropertyNames; var upm = new UserProfileManager(SPServiceContext.Current); var items = from p in upm.DefaultProfileSubtypeProperties where !p.IsSection orderby p.DisplayOrder select new ListItem( string.Format("{0} ({1})", p.DisplayName, p.Name), p.Name) { Selected = names.Contains(p.Name) }; this.list.Items.AddRange(items.ToArray()); this.Controls.Add(this.list); } public override void ApplyChanges() { var set = new HashSet<string>(this.list .GetSelectedIndices() .Select(i => this.list.Items[i].Value)); this.WebPart.ProfilePropertyNames = set; } }
В этом Tool Part создается список с возможностью множественного выбора, который заполняется свойствами профилей пользователей.
Чтобы отображать этот Custom Tool Part необходимо веб-часть унаследовать от Microsoft.SharePoint.WebParts.WebPart и переопределить метод GetToolParts
public override ToolPart[] GetToolParts() { var tps = base.GetToolParts().ToList(); tps.Add(new OrganizationTreeToolPart()); return tps.ToArray(); }
Далее чтобы использовать это свойство достаточно слегка изменить возвращаемую схему
public PropertyDescriptorCollection Schema { get { if (this.schema == null) { var names = this.ProfilePropertyNames; //(1) var upm = new UserProfileManager(SPServiceContext.Current); //Filter section headers from property list var props = from prop in upm.DefaultProfileSubtypeProperties where !prop.IsSection where names.Contains(prop.Name) //(2) orderby prop.DisplayOrder select new UserProfilePropertyDescriptor(prop); this.schema = new PropertyDescriptorCollection(props.ToArray()); } return this.schema; } }
Изменения выделены комментариями.
Теперь пользователь сможет самостоятельно выбирать какие свойства профиля отправлять веб-части получателю.
Асинхронная загрузка дерева организаций по требованию
В первоначальном варианте дерево строилось при загрузке веб-части, но необходимости в этом нет. Достаточно построить дерево, из части узлов, а остальное загружать по требованию.
И снова нам на помощь приходит ASP.NET. Стандартный контрол TreeView умеет загружать узлы по требованию с помощь AJAX. Нам для этого надо сделать совсем мало:
- Установить свойство TreeView.PopulateNodesFromClient = true
- Указывать TreeNode .PopulateOnDemand = true, если требуется загрузка узлов по требованию
- Обрабатывать событие TreeView.TreeNodePopulate, в обработчике которого и заниматься загрузкой узлов.
Код для загрузки по требованию
protected override void CreateChildControls() { this.tree = new TreeView(); this.tree.EnableClientScript = true; this.tree.PopulateNodesFromClient = true; this.tree.TreeNodePopulate += new TreeNodeEventHandler(tree_TreeNodePopulate); this.tree.Nodes.Add(ToTreeNode(this.OrganizationProfileManager.RootOrganization)); Controls.Add(this.tree); } void tree_TreeNodePopulate(object sender, TreeNodeEventArgs e) { var recordId = int.Parse(e.Node.Value); var profile = this.OrganizationProfileManager.GetOrganizationProfile(recordId); foreach (var child in profile.GetChildren()) { e.Node.ChildNodes.Add(ToTreeNode(child)); } } private static TreeNode ToTreeNode(OrganizationProfile child) { return new TreeNode(child.DisplayName, child.RecordId.ToString()) { PopulateOnDemand = child.HasChildren, }; }
Bin deployment
Сборки решений уровня фермы в SharePoint можно развертывать как в GAC, так и в каталог Bin веб-приложений SharePoint. Второй вариант подходит только для веб-частей, но в данном случае больше и не нужно.
Чтобы развернуть сборку в Bin надо поменять одну настройку в свойствах проекта
Но после этого решение откажется работать. Для сборок, развертываемых в Bin, необходимо вручную прописывать требуемый уровень разрешений для работы. Это можно сделать руками, но можно воспользоваться расширением SPSF.
В результате в solution manifest (package) должен появиться раздел
<CodeAccessSecurity> <PolicyItem> <PermissionSet class="NamedPermissionSet" version="1"> <IPermission class="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" version="1" ObjectModel="True" /> <IPermission class="SecurityPermission" version="1" Flags="Execution" /> <IPermission class="AspNetHostingPermission" version="1" Level="Minimal" /> </PermissionSet> <Assemblies> <Assembly Name="$SharePoint.Project.AssemblyName$" Version="$SharePoint.Project.AssemblyVersion$" PublicKeyBlob="$SharePoint.Project.AssemblyPublicKeyBlob$" /> </Assemblies> </PolicyItem> </CodeAccessSecurity>
Кстати SPSF генерирует переносы строк, но с ними не работает.
Заключение
Если вы дочитали до этого момента и ничего не поняли, то рекомендую более внимательно изучать ASP.NET Controls и .NET FW в частности компонентную модель, вопросы работы с данными и безопасностью, а также внимательно изучить ссылки, которые я привел в первом посте.
Исходный код можно найти на CodePlex: http://spsamples.codeplex.com/
Решение задач. Соединенные веб-части.
Ранее я приводил список задач для проверки навыков программирования для 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 и других интересных вещах, а также где взять исходники этого проекта.