пятница, 9 января 2009 г.

Конфигурирование

Конфигурирование Listma на данный момент вызывает наибольшее количество вопросов. Поэтому, рассмотрим это вопрос подробнее.
Идеалогия фрэймворка исходит из того, что существует stateful бизнес-объект (entity) который ничего не знает о Listma и vice-versa. Следовательно, нам нужно конфигурировать Listma перед использованием.
Собственно конфигурирование состоит из двух частей:
1. Описать диаграму состояний (statechart) бизнес-объекта, состоящую из состояний и переходов.
2. Описать EntityWorkflow для объекта. Здесь мы указываем, как Listma будет взаимодействовать с бизнес-объектом: указываем тип объекта, даем ссылку на диаграму состояний, описываем маппинг на свойство/поле где объект хранит свое состояние, указываем класс-провайдер ролей, и т.д.

Конфигурирование Listma может выполняться двумя способами:
- декларативно, при помощи фигурационных xml файлов
- императивно, в runtime и в исходном коде.

Общий сценарий конфигурирования



Вся работа с Listma осуществляется через класс ListmaManager. При созданни экземпляра ListmaManager, ему явно или неявно передается экземпляр интерфейса IConfigProvider, который предоставляет доступ к конфигурационной информации. В состав фрэймворка входит реализация этого интерфейса - класс ConfigProvider.
У ListmaManager три конструктора. Первый:

public ListmaManager()
{
_configProvider = ConfigProvider.GetDefault();
}

использует синглтон ConfigProvider.GetDefault(). Как следствие этого, во-первых, в этом случае ищется конфигурационный файл с именем "listma.config" в директории AppDomain.Basedirectory. Во-вторых, ConfigProvider кэширует загружаемые из xml диаграмы сотояний (statecharts) и поэтому, все экземпляры ListmaManager созданные этим конструктором используют общий statechart кэш.

Второй конструктор

public ListmaManager(string configFileName)

принимает в качестве параметра имя файла конфигурации, создает экземпляр ConfigProvider и загружает в него данные конфигурации.

Третий конструктор

public ListmaManager(IConfigProvider configProvider)

принмает в качестве параметра экземпляр IConfigProvider. Т.е. вы можете создать свою реализацию IConfigProvider либо, сконфигурировать экземпляр ConfigProvider в коде и передать его в конструктор.

Кофигурирование через xml файлы.



Данный сценарий не представляет сложности, достаточно вызвать конструктор

public ListmaManager(string configFileName)
с именем конфигурационного файла. Поэтому мы сосредоточимся на рассмотрении формата конфигурационного файла.
В исходниках проекта, а также в инсталлятор, входит xml схема конфигурационного файла ListmaConfiguration.xsd
Корневой элемент ListmaConfiguration содержит два атрибута:
- StatechartDir - путь к папке, в которой Listma будет искать файлы с описанием диаграм состояний (statechart). Если атрибут не задан используется AppDomain.BaseDirectory.
- DefaultPermissionLevel - дефолтный UIPermissionLevel который возвращает IPermissionProvider для тех элементов UI, праа доступа к которым не описаны в диаграме состояний (statechart). Если атрибут не задан, используется значение UIPermissionLevel.Hidden.

Далее следуют элементы EntityWorkflow, каждый из которых описывает один workflow:
- EntityType(обязательный атрибут)- идентификатор типа бизнес-объекта, с которым связан данный workflow. По умолчанию в качестве идентификтора используется полное имя типа бизнес-объекта (Type.FullName). Если у вас только один workflow для каждого типа бизнес-объекта, используйте Type.FullName в качестве идентификатора типа. Если для одного типа бизнес-объекта вам требуется сконфигурировать несколько workflow, вы можете указывать здесь любой уникальный строковый идентификатор. В этом случае вам придется использовать перегруженные версии методов ListmaManager с параметром string entityType. Например:

public IWorkflowAdapter<EntityType> GetWorkflow<EntityType>(EntityType entity, string entityType)

вместо

public IWorkflowAdapter<EntityType> GetWorkflow<EntityType>(EntityType entity)

- StatechartId(обязательный атрибут) - Id диаграмы состояний данного workflow. Имя файла диаграмы должно совпадать со StatechartId (плюс расширение .xml), а сам файл должен распологаться в каталоге, указанном в атрибуте StatechartDir
- StateMap (обязательный атрибут) - имя атрибута бизнес-объекта (public свойство (get; set;) или поле), используемого для хранения состояния. Поле/свойство может иметь тип string, int, enum. В это поле будет сохранятся идентификатор текущего состояния, и это надо учитывать при создании диаграмы состояний.
- StatechartMap - имя атрибута бизнес-объекта (public свойство (get; set;) или поле) используемого для хранения StatechartId заданного при старте workflow объекта (метод ListmsManager.StartWorkflow). Данный маппинг нужен, если вы хотите обеспечить версионность workflow. При этом StatechartId будет сохранен в экземпляре бизнес-объекта и в течении всей жизни этого экземпляра будет использоваться именно этот statechart, даже если в конфигурации будет указан другой StatechartId.
- InitialState - указывает Id состояния, которое будет задано экземпляру бизнес-объекта при старте workflow. Если атрибут не задан (или имеет значение "*"), в качестве стартового будет использовано первое состояние с признаком Initial="true" в диаграме состояний.
- WorkflowFactoryClass - имя класса, реализующего интерфейс IWorkflowFactory для данного типа бизнес-объекта. Если атрибут не задан, используется ReflectionFactory и соответственно, реализация IWorkflowAdapter на reflection.
- RoleProviderClass - имя класса реализующего интерфейс IRoleProvider для данного типа бизнес-объекта. Если атрибут не задан, используется дефолтная реализация которая использует Thread.CurrentPrincipal.IsInRole для определения принадлежности к роли.

Конфигурирование в коде



Listma позволяет полностью отказаться от использования конфигурационных файлов и выполнять конфигурирование в коде.
Для того, чтобы сконфигурировать workflow для бизнес-объекта, прежде всего следует создать экземпляр EntityWorkflow. Минимально необходимый набор параметров передается в единственный public конструктор:

public EntityWorkflow(string entityType, string statechartId, string stateMap)

остальные параметры конфигурации задаются через методы класса EntityWorkflow.
Сконфигурированный EntityWorkflow регистрируем в ConfigProvider:

EntityWorkflow orderWorkflow = new EntityWorkflow(typeof(Order).FullName, "OrderWorkflow1", "State");
orderWorkflow.RegisterWorkflowFactoryType(typeof(OrderWorkflowFactory));
ConfigProvider config = new ConfigProvider();
config.RegisterEntityWorkflow(orderWorkflow);

Если более ничего не сделать, Listma будет искать файл OrderWorkflow1.xml для загрузки диаграмы состояний. Однако диаграму состояний можно создать непосредственно в коде, в модном нынче стиле fluent interface.
Мне очень не хотелось нагружать классы метаданных диаграмы состояния специфическими методами, но по счастью в C# есть extension methods. Работу по созданию диаграмы состояния в стиле fluent interface взял на себя класс StatechartBuilder.

Создание диаграмы начинается с метода расширения BuildStatechart для IConfigProvider. Метод принимает в качестве параметра экземпляр EntityWorkflow. Таким образом создаваемый Statechart берет StatechartId укаанный в EntityWorkflow и сразу регистрируется в конфигурации. В дальнейшем на этот Statechart наращивается "мясо", в том числе код обработчиков событий. Вот пример:

IConfigProvider config = new ConfigProvider();
EntityWorkflow orderWorkflow = new EntityWorkflow(typeof(Order).FullName, "OrderWorkflow1", "State");
config.BuildStatechart(orderWorkflow)
.WithState("New", "New", true)
.WithTransition("Activate", "Activate")
.ToState("Active")
.PerformedBy("Owner")
.PerformedBy("Manager")
.WithNotification("OnActivation")
.ToAddress("user@mail.com")
.ToRole("Reviewer")
.CcRole("Administrator")
.CcAddress("user2@mail.com")
.Ret()
.Ret()
.WithTransition("Close", "Close")
.ToState("Closed")
.Ret()
.DefinePermissionsFor("Number")
.ForRole("Owner", UIPermissionLevel.Write)
.ForRole("*", UIPermissionLevel.Read)
.Ret()
.DefinePermissionsFor("Address")
.ForRole("Owner", UIPermissionLevel.Write)
.ForRole("*", UIPermissionLevel.Read)
.Ret()
.Ret()
.WithState("Active", "Active", false).Ret()
.WithState("Closed", "Closed", false).Ret()
.WithNotifyTemplate("OnActivation", "Order {0} has been activated.", "Order {0} has been activated.");

Здесь, для сущности "Order", которая хранит свое состояние в свойстве "State", мы создали workflow с именем "OrderWorkflow1". Для этого workflow мы создали диаграму состояний с тремя возможными состояниями "New", "Active", "Closed". Мы описали переход из состояния "New" в состояние "Active". Указали что этот переход могут выполнять только роли "Owner" и "Manager". Также мы описали нотификационное сообщение для этого перехода, и указали получателей этого сообщения. Далее мы указали, что в состоянии "New" только пользователь с ролью "Owner" может редактировать свойства Order.Number и Order.Address, а для остальных пользователей эти свойства должны быть read-only.
Здесь, также, заслуживают пояснение методы Ret(). Поскольку структура дерева Statechart весьма развесистая, по ходу строительства приходится менять контекст: сначал строим сотояние, потом - переход, потом - нотификации перехода. Для возврата к предыдущему контексту и используется методы Ret(). Таким образом, в интелисенсе у вас всегда отображаются только "нужные" методы, а не все без разбора. Кому интересно, смотрите реализацию класса StatechartBuilder.

Здесь же, при строительстве диаграмы можно задать обработчики соответствующих событий. Причем, это можно сделать, как передавая экземпляры классов-обработчиков (реализации IHandler), так и в виде анонимных методов. Вот пример:

List<string> Log = new List<string>();
IConfigProvider config = new ConfigProvider();
EntityWorkflow workflow = new EntityWorkflow(typeof(Order).FullName, "OrderWorkflow1", "State");
config.RegisterEntityWorkflow(workflow);
config.BuildStatechart(workflow)
.WithState("Draft", "Draft", true)
.ExitHandledBy<Order, TestContext>((o, c) => Log.Add(o.State.ToString() + " exit"))
.WithTransition("Do", "Do").ToState("Archive")
.HandledBy<Order, TestContext>(o => { Log.Add("Prevalidate order " + o.Number); },
(o, c) =>
{
Console.WriteLine("Context is '{0}', Order state {1}", c.Text, o.State);
c.Text += " has been done";
},
(o, s) => { Log.Add("State is changed to " + s); return true; })
.WithNotification("Notification1")
.HandledBy<Order, TestContext>((role, o, ctx) =>
{
return new string[] { role + "@mail.com" };
},
(message, tempalte, o, ctx) =>
{
message.Subject = tempalte.Subject;
message.Body = tempalte.Body;
})
.ToRole("Role1")
.CcRole("Role2")
.Ret()
.Ret()
.Ret()
.WithState("Archive", "Archive", false)
.EnterHandledBy<Order, TestContext>((o, c) => Log.Add(o.State.ToString() + " enter"))
.Ret()
.WithNotifyTemplate("Notification1", "Subject", "Body");


Здесь, непосредственно в описании диаграммы, мы указали код, который будет выполняться при выходе объектов Order из состояния "Draft", код который будет выполняться при переходе в состояние "Archive", и код обработчика для нотификации при этом переходе.

Итак, сконфигурировать Listma можно как в коде, так и в xml файлах.
Конфигурирование в коде избавляет нас от множества ошибок, а также очень удобно для юнит тестов. Однако, если вам надо изменить диуграму состояний вам придется перекомпилировать приложение.

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

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

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