понедельник, 5 января 2009 г.

Реальный пример

Из предыдущего примера мы узнали, что при помощи Listma можно описать в xml диаграму состояний для бизнес-объекта, и после этого получить список доступных переходов и перевести бизнес объект в новое состояние. Вероятно, такого же эффекта можно было достичь и более скромными средствами. В чем же практическая ценность предлагаемого движка и как его использовать на практике?

Давайте рассмотрим реальный пример. Среди исходников проекта есть пример приложения BugTracker. Я создавал BugTracker, как "классическое" ASP.Net приложение, созданное при помощи "мышки и визарда", в том числе и для того, что-бы посмотреть насколько удобно (или нет) будет использовать Listma в таких вот "экстремальных" условиях.

Приложение BugTracker



Итак вернемся к нашему багтрэкеру. Он не так прост, как кажется сначала. Во-первых он позволяет вести баги и таски для нескольких проектов. Для каждого проекта может назначаться команда, а каждому члену команды - своя роль. В соответствии с ролью в команде проекта должны определяться права пользователя на те или иные действия с багом.
Во-вторых, мы хотели бы вести в багтрэкере различные типы записей. Сейчас их два: Task и Bug, но можно добавить новые типы, для этого есть специальная таблица в БД. Например, можно ввести две категории багов: Production Bug и Non production Bug. Разница между типами записей в их жизненном цикле, и это должно настраиваться в runtime. Данные же у всех записей одинаковые, и поэтому они представлены в приложении одной сущностью: Issue.
В-третьих, в разных проектах могут быть разные роли, и у каждой роли должен быть свой набор доступных действий при работе с Issue. Все это тоже должно настраиваться в рантайме.
Итак, мы имеем приложение с вот такой моделью домена:

(Да, да, вы не ошиблись, это Entity Fremework)
Все необходимые web формы мы создали при помощи стандартных ASP.NET компонентов и EntityDataSource-в.
Теперь нам необходимо, чтобы на форме редактирования Issue, в зависимости от Issue.Type, а также от роли текущего пользователя в проекте и от текущего состояния Issue.State, на форме должны отображаться различные кнопки-действя, а также должны становиться доступными или недоступными те или иные поля. Причем все это должно легко и непринужденно настраиваться в runtime, желательно без пересборки приложения. Вот он, здравствуй, ад разработчика!

Составляем диаграмы состояния для Task и Bug



Впрочем, у нас есть Listma. С чего начнем? Начнем с описания двух диаграм состояний, одна для Issue.Type == "Task", вторая для Issue.Type == "Bug". Диаграма состояний для Task будет включать в себя состояния: New, Assigned, Opened, Postponed, Rejected, Done. Диаграма состояний Bug включает состояния: New, Assigned, Opened, Resolved, Closed. Полностью диаграмы можно посмотреть в папке workflow проекта (файлы BugWorkflow.xml и TaskWorkflow.xml). Для каждого состояния описаны доступные переходы. Например:

<State Id="New" Title="New" Initial="true">
<Transition Id="Assign" Title="Assign" TargetState="Assigned" >
<Performers>
<Performer Role="Manager" />
<Performer Role="Owner" />
</Performers>
</Transition>
</State>

Элемент <Performers> задает роли, которым доступен данный переход. Тут следует заметить, что помимо командных ролей, мы вводим две контекстные роли: Owner - пользователь создавший запись (Issue.Owner) и Performer - пользователь назначенный исполнять задачу или баг (Issue.AssignedTo). Как это все работает, мы разберемся позже.
Сейчас для нас важно понять, что перечень элементов Transition в диаграме состояний должен превратиться в набор кнопок-действий на форме редактирования Issue. Впоследствии, просто изменяя xml файл, мы сможем менять поведение нашего багтрэкера.

Другой важный кусок xml диаграмы состояний, это описания доступа к UI элементам для каждого состояния:

<State Id="New" Title="New" Initial="true">
<UIPermissions>
<UIElement Name="Id">
<Permission Role="*" Level="Read" />
</UIElement>
<UIElement Name="Type">
<Permission Role="*" Level="Read" />
<Permission Role="Owner" Level="Write" />
</UIElement>
<UIElement Name="Short">
<Permission Role="*" Level="Read" />
<Permission Role="Owner" Level="Write" />
</UIElement>
<UIElement Name="Description">
<Permission Role="*" Level="Read" />
<Permission Role="Owner" Level="Write" />
</UIElement>
<UIElement Name="State">
<Permission Role="*" Level="Read" />
</UIElement>
<UIElement Name="Result">
<Permission Role="*" Level="Read" />
</UIElement>
<UIElement Name="CreateDate">
<Permission Role="*" Level="Write" />
</UIElement>
<UIElement Name="Project">
<Permission Role="*" Level="Read" />
</UIElement>
<UIElement Name="Owner">
<Permission Role="*" Level="Read" />
<Permission Role="Owner" Level="Write" />
</UIElement>
<UIElement Name="AssignedTo">
<Permission Role="*" Level="Read" />
<Permission Role="Manager" Level="Write" />
</UIElement>
</UIPermissions>
</State>

Для каждого элемента (в нашем случае их имена совпадают с именами свойств класса Issue, но это не принципиально) описан перечень ролей и уровень доступа для каждой из них. Уровень доступа определяется енумом:

public enum UIPermissionLevel
{
Hidden = 0,
Read = 1,
Write = 2
}

В Listma нет готовых механизмов для разграничения доступа к полям Winform или ASP webform, но есть все необходимое чтобы такой механизм реализовать для любого типа представления. Сейчас мы рассмотрели, как декларируются права доступа. Пример того, как реализовать механизм проверки ролей для ASP.Net мы рассмотрим позже.

Конфигурируем framework



После того, как созданы xml диаграмы состояний, необходимо сконфигурировать framework (файл /workflow/workflow.config):

<?xml version="1.0" encoding="utf-8" ?>
<ListmaConfiguration xmlns="urn:Listma:configuration"
StatechartDir="workflow"
DefaultPermissionLevel="Write">
<EntityWorkflow EntityType="Bug"
StatechartId="BugWorkflow"
StateMap="State" />
<EntityWorkflow EntityType="Task"
StatechartId="TaskWorkflow"
StateMap="State" />
</ListmaConfiguration>

Атрибут "StatechartDir" указывает путь к каталогу в котором LIstma ищет файлы диаграм состояния.
Элементы "EntityWorkflow" описывают два workflow для разных "EntityType" (несмотря на то, что оба будут применяться к одному и тому же типу - Issue).
Атрибуты "StatechartId" указывают идентификаторы диаграм состояний, которые должны совпадать с именами xml файлов этих диаграм.
Атрибуты "StateMap" определяют поле класса Issue, которое используется для хранения состояния.
Итак диаграмы состояний определены, framework сконфигурирован. Следующая задача разместить на форме Issue список доступных пользователю команд.

UserControl "Список действий"

Для размещения на форме Issue списка команд, которые доступны текущему пользователю в данном состоянии Issue, мы сделаем UserControl - WfActions.ascx. Контрол очень прост, представляет собой Repeater:

<asp:Repeater ID="Repeater1" runat="server" DataSourceID="WfActionsDataSource"
onitemcommand="Repeater1_ItemCommand">
<HeaderTemplate>
<div>Available actions:</div>
</HeaderTemplate>
<ItemTemplate>
<table>
<tr><td>
<asp:Button ID="ActionButton" runat="server" Width="160 px" CssClass="button"
CommandName="WfAction"
CommandArgument='<%# DataBinder.Eval(Container.DataItem, "Id" ) %>'
Text='<%# DataBinder.Eval(Container.DataItem, "Title" ) %>'
/>
</td></tr>
</table>
</ItemTemplate>
</asp:Repeater>

А чтобы заполнить репитер, используем ObjectDataSource, для которого напишем соответствующий метод бизнес логики:

01 public static class IssueService
02 {
03 public static TransitionInfo[] GetActions(int issueId)
04 {
05 if (issueId == 0) return new TransitionInfo[] { };
06 using (DBContext ctx = new DBContext())
07 {
08 Issue issue = ctx.Issue.Include("Type").Where(i => i.Id == issueId).First();
09 ListmaManager listma = new ListmaManager(GetListmaConfiguration());
10 IWorkflowAdapter wf = listma.GetWorkflow(issue, issue.Type.Type);
11 return listma.GetAvailableTransitions(wf, ctx);
12 }
13 }
14 ...
15 }

Этот код рассмотрим подробнее. В шестой строке создаем EF контекст. В восьмой - получаем экземпляр Issue. В строке 9 мы создаем экземпляр ListmaManager, через который ведется вся работа с фрэймворком. Далее нам надо обернуть экземпляр Issue в IWorkflowAdapter<> с которым умеет работать Listma, или иными словами - получить экземпляр workflow. Для этого мы используем метод listma.GetWorkflow(issue, issue.Type.Type). Вторым параметром string entityType мы передаем issue.Type.Type. Здесь следует вспомнить, что в конфигурации Listma мы указали в качестве EntityType значения "Task" и "Bug", которые совпадают со значениями IssueType хранящимеся в базе. Таким образом, в зависимости от типа Issue, мы будем получать различные workflow, связанные с различными диаграмами состояний.
В строке 11 мы получаем список доступных переходов для данного Issue вызовом метода listma.GetAvailableTransitions(wf, ctx). Вторым параметром, как видим, передается контекст вызова. В данном случае это EF контекст. Где он нам может понадобиться, мы увидем далее. Впрочем, для случаев, когда контекст вызова нам не важен, есть вариант GetAvailableTransitions с одним параметром.
Полученный контрол WfActions.ascx вставляем на форму Issue.aspx:

Включаем - не работает. Наш метод IssueService.GetActions(int) всегда возвращает пустой массив, не зависимо от состояния Issue и текущего пользователя. Дело в том, что мы забыли реализовать одну малую деталь. В xml диаграмах мы описали для каких ролей какие переходы доступны. Но как в runtime определить текущие роли пользователя, тем более в контексте конкретного Issue?
Для этого мы должны реализовать свой IRoleProvider и назначить его для шаших workflow.

IRoleProvider



Listma распологает встроенной реализацией IRoleProvider. Для определения принадлежности пользователя к толи, он использует Thread.CurrentPrincipal.IsInRole(). Естественно, что этот провайдер ничего не знает о тех ролях, которые мы определили в своем приложении. Нам необходимо реализовать своего провайдера:

public class IssueRoleProvider : IRoleProvider
{
#region IRoleProvider Members

public bool IsInRole(string roleName, Issue entity, DBContext context)
{
string userName = System.Threading.Thread.CurrentPrincipal.Identity.Name;
bool result = false;
if(string.IsNullOrEmpty(userName)) return result;
switch (roleName)
{
case "Owner":
if (!entity.OwnerReference.IsLoaded) entity.OwnerReference.Load();
result = userName == entity.Owner.Name;
break;
case "Performer":
if (!entity.AssignedToReference.IsLoaded) entity.AssignedToReference.Load();
if (entity.AssignedTo != null) result = userName == entity.AssignedTo.Name;
break;
default:
if (!entity.ProjectReference.IsLoaded) entity.ProjectReference.Load();
result = UserService.IsUserInRole(userName, roleName, entity.Project.Id, context);
break;
}
return result;
}

#endregion
}

Как видим, IRoleProvider это generic интерфейс с двумя type-параметрами. Первый - это тип класса, к которому мы подключаем workflow. Второй - это тип контекста вызова. Дело в том, что большинство методов ListmaManager имеют перегруженные варианты с параметром контекста. Этот контекст передается в методы классов-расширений, вроде нашего IssueRoleProvider. Если мы не намерены использовать контекст, нам все равно придется реализовать generic интерфейс с двумя параметрами, но тип второго параметра будет object: IRoleProvider.
Вернемся к IssueRoleProvider. В нем мы определяем имя текущего пользователя как Thread.CurrentPrincipal.Identity.Name (заметьте, имя не приходит к нам в параметрах, и как определить его - наша забота), а также принадлежность пользователя к двум контекстным ролям "Owner" и "Performer". Определение принадлежности пользователя в остальным проектным ролям мы делегируем методу бизнес логики приложения UserService.IsUserInRole().
Теперь нам надо указать Listma, что для наших workflow следует использовать IssueRoleProvider. В конфигурационном файле для этих целей служит атрибут RoleProviderClass элемента EntityWorkflow:

<EntityWorkflow EntityType="Bug"
StatechartId="BugWorkflow"
StateMap ="State"
RoleProviderClass="BugTracker.Workflow.IssueRoleProvider, BugTracker" />

После всех этих манипуляций, мы наконец получаем работающий список действий на форме Issue.
Примечание. Для того, чтобы увидеть как работает данный функционал, вам следует запустить приложение BugTracker и зарегистрироваться новым пользователем (Home -> Login -> Register). Затем, от имени этого пользователя создайте новый баг, Измените свойство Owner бага, выполните Logout и сравните, как изменяется список кнопок на форме редактирования Issue.

Продолжение следует.

Комментариев нет:

Отправить комментарий