Сообщений 0    Оценка 46 [+0/-1]         Оценить  
Система Orphus

Windows Workflow Foundation

Глава из книги “C# 2005 и платформа .NET 3.0 для профессионалов”

Авторы: Кристиан Нейгел
Билл Ивьен
Джей Глинн
Морган Скиннер
Карли Уотсон

Источник: C# 2005 и платформа .NET 3.0 для профессионалов
Материал предоставил: Издательство ''Вильямс''
Опубликовано: 03.11.2007
Исправлено: 15.04.2009
Версия текста: 1.0
Пример “Hello World”
Действия
IfElseActivity
ParallelActivity
CallExternalMethodActivity
DelayActivity
ListenActivity
Модель выполнения действий
Пользовательские действия
Верификация действий
Темы и конструкторы
ActivityToolboxItem и пиктограммы
Пользовательские составные действия
Рабочие потоки
Последовательные рабочие потоки
Рабочие потоки типа конечных автоматов
Передача параметров рабочему потоку
Возврат результатов из рабочего потока
Привязка параметров к действиям
Исполняющая среда рабочего потока
Службы рабочих потоков
Служба постоянства
Служба отслеживания
Пользовательские службы
Хостинг рабочих потоков
Конструктор Workflow Designer
Резюме
Указания по загрузке

В настоящей главе мы представим обзор рабочего потока Windows Workflow (далее в этой главе — WF), являющийся моделью, в которой вы можете определять и выполнять процессы с использованием набора строительных блоков — действий (activities). WF предлагает визуальный конструктор, который по умолчанию развернут в среде Visual Studio и позволяет перетаскивать действия из панели инструментов на поверхность конструктора, создавая шаблон рабочего потока.

Этот шаблон затем может быть выполнен за счет создания экземпляра рабочего потока WorkflowInstance и запуска этого экземпляра. Код, выполняющий рабочий поток, известен как исполняющая среда рабочего потока — WorkflowRuntime, и этот объект также может содержать в себе множество служб, к которым может обращаться рабочий поток. В любое время может одновременно выполняться несколько экземпляров рабочего потока, и исполняющая среда занимается планированием этих экземпляров, сохраняя и восстанавливая их состояние; она также может записывать поведение каждого экземпляра рабочего потока по мере его работы.

Рабочий поток конструируется из ряда действий, и эти действия осуществляются в исполняющей среде. Действием может быть отправка электронной почты, обновление строки в базе данных либо выполнение транзакции в системе заднего плана. Существует ряд встроенных действий, которые могут быть использованы для выполнения работ общего назначения, и, кроме того, при необходимости вы можете создавать собственные действия и подключать их к рабочему потоку.

Эта глава посвящена обсуждению различных типов рабочих потоков, которые доступны в готовом виде, а также описанию некоторых встроенных действий, доступных в WF. Вы увидите, как работать с некоторыми из стандартных типов действий, а также создадите два собственных действия, чтобы ознакомиться с принципом расширения WF. Мы начнем с канонического примера, с которым имеет дело каждый, кто впервые сталкивается с новой технологией, — “Hello World”, — а также опишем все, что вам понадобится для получения исполняющегося рабочего потока на вашей машине разработчика.

Пример “Hello World”

Чтобы начать работу с WF, вам понадобится загрузить как Windows .NET Framework 3.0, так и Visual Studio 2005 Extensions for Windows Workflow Foundation. Первое загружать не нужно, если вы работаете в среде Windows Vista, поскольку .NET Framework включен в стандартную поставку этой операционной системы. Visual Studio 2005 Extensions for Windows Workflow Foundation — это установочный пакет, добавляющий поддержку рабочих потоков в Visual Studio, и подробности его загрузки вы найдете в конце настоящей главы. После того, как все это будет инсталлировано, вы увидите новый набор типов свойств в узле Workflow (Рабочий поток) внутри диалога New Project (Новый проект), как показано на рис. 41.1.


Рис. 41.1. Новый набор проектов в узле Workflow

Выберите из доступных шаблонов Sequential Workflow Console Application (это создаст консольное приложение, которое будет служить хостом для исполняющей среды рабочего потока) и рабочий поток по умолчанию, на который вы сможете потом перетаскивать действия.

Затем перетащите действие Code из панели инструментов на поверхность конструктора — так, чтобы получить рабочий поток вроде того, что показан на рис. 41.2.


Рис. 41.2 Рабочий поток с действием Code

Восклицательный знак в верхнем правом углу действия указывает на то, что обязательное свойство действия еще не было определено. В данном случае это свойство ExecuteCode, указывающее метод, который будет вызван при выполнении действия.

О пометке вашего собственного свойства как обязательного вы узнаете в разделе, посвященном верификации действия. В результате выполнения двойного щелчка на действии кода будет создан нужный метод в отделенном классе, и в нем вы можете использовать Console.Writeline для вывода строки “Hello World”, как показано в приведенном ниже фрагменте кода.

private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("Hello World");
}

Если вы затем соберете и запустите приложение, то увидите текстовый вывод на консоли. Когда программа выполняется, создается экземпляр WorkflowRuntime, а затем конструируется и выполняется экземпляр вашего рабочего потока. Когда активизируется код действия, он вызывает определенный нами метод, который выводит строку на консоль. В разделе “Исполняющая среда рабочего потока” мы опишем в деталях, как организовать хост для исполняющей системы.

Действия

Все, что происходит в рабочем потоке — это действия. Сам рабочий поток представляет собой действие, причем действие специфического типа, которое обычно позволяет определять внутри него другие действия; это известно как составное действие (composite activity), и позднее в главе будут даны примеры других составных действий. Действие — это просто класс, который обязательно наследуется от класса Activity.

Класс Activity определяет ряд перегружаемых методов и, вероятно, наиболее важный из них — метод Execute, представленный в следующем фрагменте:

protected override ActivityExecutionStatus Execute
   ( ActivityExecutionContext executionContext )
{
   return ActivityExecutionStatus.Closed;
}

Когда исполняющая система планирует действие к выполнению, обязательно вызывается метод Execute, и именно здесь вы имеет возможность поместить собственный код, реализующий поведение вашего действия. В примере из предыдущего раздела, когда исполняющая система рабочего потока вызывает Execute из CodeActivity, то реализация этого метода запустит метод, определенный в отделенном классе, который, в свою очередь, выведет строку на консоль.

Методу Execute передается параметр контекста типа ActivityExecutionContext, о котором вы узнаете больше на протяжении этой главы. Этот метод имеет тип возврата типа ActivityExecutionStatus, который используется исполняющей системой, чтобы определить, завершилось ли действие успешно, все еще продолжается, либо пребывает в одном из других возможных состояний, которые могут описать исполняющей системе рабочего потока, как обстоят дела с данным действием. Возврат из этого метода ActivityExecutionStatus.Closed говорит о том, что действие завершило свою работу и может быть закрыто.

Существуют 28 стандартных действий, представленных WF, и в следующем разделе мы приведем примеры некоторых из них вместе со сценариями, где вы можете их использовать. Соглашение по именованию для действий требует добавления суффикса Activity к имени; например, код действия, показанный на рис. 41.2, определен классом CodeActivity.

Все стандартные действия определены в пространстве имен System.Workflow. Activities, которое является частью сборки System.Workflow.Activities.dll. Есть две другие сборки, которые составляют WF. Это System.Workflow.ComponentModel.dll и System.Workflow.Runtime.dll.

IfElseActivity

Как следует из наименования, это действие работает подобно оператору if-else из C#.

Когда вы поместите действие IfElseActivity на поверхность конструктора, то увидите примерно то, что показано на рис. 41.3.


Рис. 41.3. Действие IfElseActivity

IfElseActivity — составное действие, потому что оно создает две ветви (которые сами по себе представляют действие определенного типа; в данном случае — IfElseBranchActivity). Каждая ветвь также является составным действием, унаследованным от SequenceActivity — класса, выполняющего последовательность действий одно за другим, сверху вниз. Как видно на рисунке, конструктор добавляет текст Drop Activities Here (Поместите сюда действия), указывая места, куда должны добавляться дочерние действия.

Первая ветвь, показанная на рис. 41.3, содержит восклицательный знак, указывающий на необходимость определения свойства Condition (условие). Условие наследуется от ActivityCondition и используется для определения того, какая ветвь должна выполняться.

Когда выполняется IfElseActivity, оно оценивает условие первой ветви, и если оно истинно, тогда эта ветвь выполняется. Если же условие оценивается как ложное, то IfElseActivity проверяет следующую ветвь, и так далее — до тех пор, пока не доберется до последней ветви в действии. Следует отметить, что IfElseActivity может иметь любое количество ветвей, каждая со своим собственным условием. Последней ветви условие не нужно, поскольку она работает как часть else в операторе if-else. Чтобы добавить новую ветвь, вы можете отобразить контекстное меню для действия и выбрать в нем Add Branch (Добавить ветвь) — этот пункт также доступен из меню Workflow (Рабочий поток) внутри Visual Studio. По мере добавления ветвей каждая из них, за исключением последней, будет иметь обязательное условие.

В WF определено два стандартных типа условий — CodeCondition и RuleCoditionReference. Класс CodeCondition выполняет метод вашего отделенного класса, который может вернуть true или false. Чтобы создать CodeCondition, отобразите таблицу свойств для IfElseActivity и установите условие Condition в Code Condition, затем введите имя кода, который должен быть исполнен, как показано на рис. 41.4.


Рис. 41.4. Установка условия Condition

Когда вы введете имя метода в таблицу свойств, то конструктор сконструирует метод в вашем отделенном классе, как показано в следующем фрагменте:

private void InWorkingHours(object sender, ConditionalEventArgs e)
{
   int hour = DateTime.Now.Hour;
   e.Result = ((hour >= 9) && (hour <= 17));
}

Приведенный код устанавливает свойство Result переданного экземпляра ConditionalEventArgs в true, если текущий час находится в диапазоне от 9 утра до 5 вечера. Условия могут быть определены в коде, как показано здесь, но есть и другой вариант — когда условие базируется на правиле (Rule), которое оценивается аналогичным образом. Конструктор Workflow Designer содержит редактор правил, который может быть использован для объявления условий и операторов (во многом подобно оператору if-else, приведенному выше). Эти правила оцениваются во время исполнения на основе текущего состояния рабочего потока.

ParallelActivity

Это действие позволяет определить набор действий, выполняемых в параллельной, или, скорее, в псевдопараллельной манере. Когда исполняющая среда рабочего потока планирует действие, она делает это в единственном потоке. Этот поток выполняет первое действие, затем второе и так далее, пока не будут завершены все действия (или пока действие ожидает некоторого ввода). Когда активизируется ParallelActivity, оно проходит по всем ветвям и планирует выполнение каждой из них по очереди. Исполняющая среда рабочего потока поддерживает очередь запланированных к выполнению действий для каждого экземпляра рабочего потока и обычно выполняет их в режиме FIFO (первый пришел, первый обслужен).


Рис. 41.5. Действие ParallelActivity

Если предположить, что у вас есть ParallelActivity, как показано на рис. 41.5, то это запланирует выполнение sequenceActivity1 и затем sequenceActivity2. Тип SequenceActivity работает, планируя выполнение первого действия вместе с исполняющей средой, и когда оно закончится, планирует второе действие. Этот метод “планирование/ожидание завершения” используется для прогона всех дочерних действий последовательности до тех пор, пока все они не будут выполнены, и тогда все последовательное действие может завершиться.

Говоря о том, что SequenceActivity планирует выполнение по одному действию за раз, подразумевается, что очередь, поддерживаемая WorkflowRuntime, постоянно обновляется запланированными действиями.

Предположим, что у нас есть параллельное действие P1, которое содержит две последовательности — S1 и S2, каждая из которых состоит из действий кода C1 и С2. Это даст нам очередь запланированных действий, показанную в табл. 41.1.

Очередь рабочего потокаИзначально в очереди нет никаких действий
P1 Параллельное действие запускается вместе с рабочим потоком
S1, S2Добавляется в очередь при выполнении P1
S2, S1.C1S1 выполняется и добавляет в очередь S1.C1
S1.C1, S2.C1S2 выполняется и добавляет в очередь S2.C1
S2.C1, S2.C2S1.C1 завершается, подходит очередь S1.C2
S1.C2, S2.C2S2.C1 завершается, подходит очередь S2.C2
S2.C2Последний элемент очереди
Таблица 41.1. Очередь запланированных действий

Здесь очередь сначала обрабатывает первый элемент (параллельное действие P1), которое добавляет два последовательных действия S1 и S2 в очередь рабочего потока. При выполнении действия S1 оно помещает свое первое действие (S1.C1) в конец очереди, и когда это действие будет запланировано и выполнено, помещает в конец очереди свое второе действие.

Как видно в приведенном примере, выполнение ParallelActivity происходит не совсем параллельно; на самом деле оно поочередно выполняет компоненты двух последовательных ветвей. Отсюда вы можете сделать вывод, что лучше, когда отдельное действие требует минимального отрезка времени, поскольку с учетом того, что очередь каждого рабочего потока обслуживает только один поток, долго выполняющееся действие может затормозить выполнение прочих действий в очереди. Таким образом, часто возникает ситуация, когда действие может выполняться в течение произвольного отрезка времени, поэтому должен быть способ пометить действие, как “долгоиграющее”, чтобы другие действия получили возможность выполняться. Вы можете сделать это, возвратив ActivityExecutionStatus из метода Execute, которое позволяет исполняющей среде узнать о том, что вы обратитесь к ней позднее, когда данное действие завершится. Примером такого действия может служить DelayActivity.

CallExternalMethodActivity

Рабочий поток обычно нуждается в вызове методов извне рабочего потока, и такое действие позволяет определить интерфейс и метод для вызова в этом интерфейсе. WorkflowRuntime поддерживает список служб (помеченных ключом — значением System.Type), которые могут быть доступны через параметр ActivityExecutionContext, переданный методу Execute.

Вы можете определить свои собственные службы для добавления в коллекцию и затем обращаться к ним изнутри ваших собственных действий. Например, вы можете построить уровень доступа к данным, представленный как интерфейс службы, и затем подготовить различные реализации этой службы для SQL Server и Oracle. Поскольку действия просто вызывают методы интерфейса, замена SQL Server на Oracle будет совершенно прозрачной для действий, поскольку они просто вызывают методы интерфейса и ничего не знают о конкретной системе управления базами данных.

После добавления к рабочему потоку CallExternalMethodActivity вы затем определяете дваобязательных свойства — InterfaceType и MethodName. Тип интерфейса определяет, какая служба времени выполнения будет использована при выполнении действия, а имя метода определяет, какой метод этого интерфейса будет вызван.

Когда действие выполняется, оно ищет службу с определенным интерфейсом, запрашивая исполняющий контекст на предмет типа службы, и затем вызывает соответствующий метод этого интерфейса. Вы можете также передать параметры методу внутри рабочего потока — об этом мы поговорим позже, в разделе “Привязка параметров к действиям”.

DelayActivity

Бизнес-процессам часто бывает нужно подождать некоторое время перед тем, как завершиться — представьте себе использование рабочего потока для утверждения расходов. Ваш рабочий поток может послать электронное письмо вашему непосредственному начальнику, запрашивая его утверждения ваших расходов. Рабочий поток затем переходит в режим ожидания, в котором он либо ждет утверждения (или отказа), но было бы неплохо определить таймаут, чтобы если ответ не получен, скажем, в течение одного дня, отчет о расходах был бы автоматически перенаправлен более высокопоставленному начальнику в административной иерархии.

Действие DelayActivity может формировать часть этого сценария (другую часть реализует ListenActivity, о котором речь пойдет ниже), и его работа заключается в ожидании в течение определенного времени, прежде чем продолжить выполнение рабочего потока. Есть также два пути определения времени ожидания — вы можете либо установить в свойстве TimeoutDuration строку вроде “1.00:00:00” (1 день, ноль часов, ноль минут и ноль секунд), либо предоставить метод, который будет вызван при выполнении действия, который установит продолжительность ожидания в своем коде. Чтобы сделать это, вам понадобится определить значение для свойства InitializeTimeoutDuration действия задержки, что сгенерирует метод в отделенном коде, как показано в следующем фрагменте:

private void DefineTimeout(object sender, EventArgs e)
{
   DelayActivity delay = sender as DelayActivity;
   if (null != delay)
   {
      delay.TimeoutDuration = new TimeSpan(1, 0, 0, 0);
   }
}

Здесь метод DefineTimeout выполняет приведение sender к типу DelayActivity и затем в коде устанавливает его свойство TimoutDuration в значение TimeSpan. Несмотря на то что в данном примере значение жестко закодировано, более вероятно, что вы будете строить его на основе некоторых других данных — возможно, параметра, переданного в рабочий поток, или значения, прочитанного из конфигурационного файла. Параметры рабочего потока обсуждаются далее в настоящей главе, в разделе “Рабочие потоки”.

ListenActivity

Распространенная программная конструкция заключается в организации ожидания одного из набора возможных событий; примером может служить метод WaitAny класса System. Threading.WaitHandle. ListenActivity предоставляет возможность сделать это в рабочем потоке, поскольку может определить любое количество ветвей, каждая из которых основана на некотором событии.

Действие события — это такое действие, которое реализует интерфейс IEventActivity, определенный в пространстве имен System.Workflow.Activities. В настоящее время есть три таких действия, определенных как стандартные в WF, а именно: DelayActivity, HandleExternalEventActivity и WebServiceInputActivity. Ниже на рис. 41.6 показан рабочий поток, ожидающий либо внешнего ввода, либо истечения некоторого времени — это пример, подходящий для реализации утверждения расходов, о котором мы говорили ранее.


Рис. 41.6. Действие ListenActivity

В этом примере действие CallExternalMethodActivity используется в качестве первого действия в рабочем потоке, которое вызывает метод, определенный в интерфейсе службы, который запросит у руководителя утверждения расходов. Поскольку это внешняя служба, запрос может поступить в форме электронной почты, сообщения IM (instance message) либо в любом другом виде, позволяющем известить вашего руководителя о том, что ему нужно обработать запрос на расходы. Затем рабочий поток выполняет действие ListenActivity, которое ожидает ввода от этой внешней службы (утверждения или отказа), и ожидает его в течение заданного периода времени.

Когда происходит прослушивание, оно ставит в очередь ожидание как первое действие в каждой ветви, и когда произойдет ожидаемое событие, все прочие ожидания прерываются, и осуществляется обработка остальной части той ветви, где произошло событие. Таким образом, в экземпляре, где отчет о расходах был утвержден, возникает событие Approved и планируется действие PayMe. Если же, однако, ваш руководитель отвергает ваш запрос на расходы, возбуждается событие Rejected, и в данном примере происходит переход к действию Panic (паника).

И последнее: если не возникло ни одно из событий — ни Approved, ни Rejected, — тогда по истечении заданного времени ожидания DelayActivity обязательно завершается, и отчет о расходах может быть направлен другому руководителю, возможно, после выполнения его поиска вверх по иерархии в Active Directory. В данном примере пользователь видит диалог, когда выполняется действие RequestApproval, так что в случае, когда осуществляется ожидание, вам также нужно закрыть диалог, то есть выполнить действие по имени HideDialog (см. рис. 41.6).

В данном примере использованы некоторые концепции, которые пока еще не были нами раскрыты, например, как идентифицируется экземпляр рабочего потока, и как события попадают обратно в исполняющую среду рабочего потока, и в конечном итоге доставляются правильному экземпляру рабочего потока. Эти концепции будут представлены в разделе “Рабочие потоки”.

Модель выполнения действий

До сих пор в этой главе мы обсуждали только выполнение действий исполняющей системой через вызов метода Execute. На самом деле действие в процессе своего выполнения может проходить через ряд состояний, что отражено на рис. 41.7.


Рис. 41.7. Состояния действия во время его выполнения

Сначала действие инициализируется посредством WorkflowRuntime, когда исполняющая система вызывает метод Initialize этого действия. Этому методу передается экземпляр IServiceProvider, который отображает доступные службы внутри исполняющей системы. Эти службы мы обсудим позже, в разделе “Службы рабочих потоков” настоящей главы. Большинство действий ничего не делают в этом методе, но метод все же нужен, чтобы вы могли в нем произвести необходимые начальные установки. Затем исполняющая система вызывает метод Execute, и действие может вернуть любое из значений перечисления ActivityExecutionStatus. Обычно вы будете возвращать из метода значение Closed, которое говорит о том, что действие завершило свою обработку. Однако если вы вернете одно из других значений состояния, то исполняющая система использует его для определения того, в каком состоянии находится действие.

Вы можете вернуть из этого метода Executing, сообщив исполняющей системе о том, что у вас еще есть работа, которую нужно доделать — типичный пример этого, это когда у вас есть составное действие, которое должно выполнить свои дочерние элементы. В этом случае ваше действие может запланировать запуск каждого своего дочернего действия и ожидать завершения их работы прежде, чем известить исполняющую систему о том, что данное действие завершилось.

Пользовательские действия

До сих пор вы имели дело с действиями, определенными внутри пространства имен System.Workflow.Activities. В этом разделе вы узнаете, как создавать собственные действия и расширять их для удовлетворения пользовательских потребностей — как во время проектирования, так и во время выполнения.

Для начала вы создадите действие WriteLineActivity, которое можно будет использовать для вывода строки текста на консоль. Хотя этот пример достаточно тривиален, позднее он будет расширен, чтобы продемонстрировать полный набор возможностей, доступных пользовательским действиям. При создании пользовательских действий вы можете просто сконструировать класс внутри проекта рабочего потока, однакопредпочтительнее создать ваши собственные действия внутри отдельной сборки, поскольку среда времени проектирования Visual Studio (и особенно проекты рабочих потоков) загрузит действия из ваших сборок и сможет заблокировать сборку, которую вы попытаетесь обновить. По этой причине вы должны создать проект простой библиотеки классов для конструирования внутри нее ваших пользовательских действий.

Простое действие вроде WriteLineActivity будет порождено непосредственно от базового класса Activity. В следующем коде показан сконструированный класс действия и определено свойство Message, отображаемое при вызове метода Execute.

using System;
using System.ComponentModel;
using System.Workflow.ComponentModel;

namespace SimpleActivity
{
   /// <summary>
   /// Простое действие, которое во время выполнения отображает сообщение на консоли
   /// </summary>

   public class WriteLineActivity : Activity
   {
         /// <summary>
         /// Выполнение действия - отображение сообщения на экране
         /// </summary>
         /// <param name="executionContext"></param>
         /// <returns></returns>
         protected override ActivityExecutionStatus Execute
         ( ActivityExecutionContext executionContext )
         {
            Console.WriteLine( Message );
            return ActivityExecutionStatus.Closed;
         }

         /// <summary>
         /// Методы get/set отображаемого сообщения
         /// </summary>
         [ Description( "The message to display" ) ]
         [ Category( "Parameters" ) ]
         public string Message
         {

            get
            {
               return _message;
            }

            set
            {
               _message = value;
            }
      }
         /// <summary>
         /// Сохранение отображаемого сообщения
         /// </summary>
         private string _message;
   }
}

Внутри метода Execute вы можете вывести сообщение на консоль и затем возвратить состояние Closed, известив исполняющую систему о том, что действие завершено.

Также вы можете определить атрибуты свойства Message, определив для него описание и категорию; эти свойства будут использованы в таблице свойство внутри Visual Studio, как показано на рис. 41.8.


Рис. 41.8. Атрибуты свойства Message

Если вы скомпилируете это решение, то сможете добавить собственные действия к панели инструментов внутри Visual Studio, выбрав пункт из контекстного меню Choose Items (Выберите элементы) в панели инструментов и перейдя в папку, где находится сборка, содержащая действия. Все действия, содержащиеся в сборке, будут добавлены на панель инструментов.

В таком виде действие вполне удобно; однако есть несколько способов сделать его еще более дружественным к пользователю. Как вы видели на примере CodeActivity ранее в этой главе, существует несколько обязательных свойств, которые, не будучи определены, приведут к появлению знака ошибки на поверхности конструктора. Чтобы получить тот же эффект для вашего действия, вам нужно построить класс, унаследованный от ActivityValidator, и ассоциировать класс с вашим действием.

Верификация действий

Когда действие помещено на поверхность конструктора, Workflow Designer ищет в этом действии атрибуты, которые определяют класс, осуществляющий верификацию данного действия. Для верификации действия необходимо проверить, установлено ли свойство Message.

Пользовательский верификатор передается экземпляру действия, и здесь вы можете определить, какие свойства являются обязательными (если таковые есть), но не были еще определены, а также добавить ошибку в ValidationErrorCollection, используемую конструктором. Эту коллекцию затем читает Workflow Designer, и любые найденные в коллекции ошибки вызовут добавление к действию предупреждающего знака, и необязательно свяжут каждую ошибку со свойством, требующим вашего внимания.

using System;
using System.Workflow.ComponentModel.Compiler;

namespace SimpleActivity
{
   public class WriteLineValidator : ActivityValidator
   {
         public override ValidationErrorCollection Validate
         ( ValidationManager manager, object obj )
         {
            if ( null == manager )
               throw new ArgumentNullException( "manager" );

            if ( null == obj )
               throw new ArgumentNullException( "obj" );

            ValidationErrorCollection errors = base.Validate( manager, obj );

            // Привести к WriteLineActivity
            WriteLineActivity act = obj as WriteLineActivity;

            if ( null != act )
            {
               if ( null != act.Parent )
               {
                  // Проверить свойство Message

                  if ( string.IsNullOrEmpty( act.Message ) )
                     errors.Add( ValidationError.GetNotSetValidationError( "Message" ) );
               }
            }

            return errors;
         }
   }
}

Метод Validate вызывается конструктором, когда обновляется любая часть действия и также когда действие помещается на поверхность конструктора. Конструктор вызывает метод Validate и передает действие в виде нетипизированного параметра obj. В этом методе сначала проверяются переданные ему аргументы, и затем вызывается метод Validate базового класса, чтобы получить ValidationErrorCollection. Хотя это здесь и не обязательно, но если имеет место наследование от действия с множеством свойств, которые также нуждаются в верификации, то вызов метода базового класса гарантирует, что все они также будут проверены.

Приведите переданный параметр к экземпляру WriteLineActivity и проверьте, есть ли у действия родитель. Эта проверка необходима, потому что функция Validate вызывается во время компиляции действия (если это действие внутри проекта рабочего потока или библиотеки действий), а в этой точке никакого родительского действия не определено. Без этой проверки вы не сможете в действительности собрать сборку, содержащую действие и его верификатор. Этот дополнительный шаг не нужен, если типом проекта является библиотека классов.

Последний шаг — проверка установки свойства Message в значение, отличное от пустой строки; этим занимается статический метод класса ValidationError, который конструирует ошибку, указывающую на то, что свойство не было определено.

Для поддержки вашего действия WriteLineActivity необходим последний шаг — добавить к действию атрибут ActivityValidation, как показано в следующем фрагменте:

[ActivityValidator(typeof(WriteLineValidator))]
public class WriteLineActivity : Activity
{
...
}

Если вы скомпилируете приложение и затем поместите WriteLineActivity в рабочий поток, то увидите ошибку верификации, как показано на рис. 41.9; щелкнув на символе ошибки, вы перейдете к соответствующему свойству в таблице свойств.

Если вы введете некоторый текст в свойстве Message, то ошибка верификации исчезнет, и вы сможете скомпилировать и запустить приложение.

Теперь, когда вы имеете готовую верификацию действия, следующее, что нужно изменить — это поведение визуализации действия, чтобы добавить цвет его заливки. Чтобы сделать это, вам нужно определить два класса ActivityDesigner и ActivityDesignerTheme, как описано в следующем разделе.

Темы и конструкторы

Экранное отображение действия выполняется с использованием класса ActivityDesigner, при этом также может использоваться класс ActivityDesignerTheme.

Класс темы используется для простого изменения поведения отображения действия внутри Workflow Designer.

public class WriteLineTheme : ActivityDesignerTheme
{
   /// <summary>
   /// Конструирование темы и установка ряда значений по умолчанию
   /// </summary>
   /// <param name="theme"></param>
   public WriteLineTheme(WorkflowTheme theme)
      : base(theme)
   {
      this.BackColorStart = Color.Yellow;
      this.BackColorEnd = Color.Orange;
      this.BackgroundStyle = LinearGradientMode.ForwardDiagonal;
   }
}

Тема унаследована от ActivityDesignerTheme, который имеет конструктор, принимающий аргумент WorkflowTheme. Внутри конструктора устанавливаются начальный и конечный цвета действия, а затем определяется кисть линейного градиента, используемая для окраски фона.

Класс Designer служит для переопределения поведения визуализации действия — в данном случае, никакого переопределения не требуется, так что следующего кода будет вполне достаточно:

[ActivityDesignerTheme(typeof(WriteLineTheme))]
public class WriteLineDesigner : ActivityDesigner
{}

Обратите внимание, что тема ассоциирована с конструктором посредством атрибута ActivityDesignerTheme.

Последний шаг состоит в том, чтобы снабдить действие атрибутом Designer:

[ActivityValidator(typeof(WriteLineValidator))]
[Designer(typeof(WriteLineDesigner))]
public class WriteLineActivity : Activity
{
...
}


Рис. 41.9. Ошибка верификации

Когда все это будет готово, действие станет отображаться в конструкторе так, как показано на рис. 41.10.


Рис. 41.10. Добавление конструктора и темы

С таким добавлением конструктора и темы наше действие теперь выглядит гораздо более профессионально. Есть много других свойств, доступных в теме, таких как перо, используемое для рисования рамки, цвет рамки и ее стиль.

Переопределяя метод OnPaint класса ActivityDesigner, вы можете получить полный контроль над отображением действия. Вам предоставляется возможность самостоятельно поупражняться в этом; создайте действие, совершенно непохожее на любые другие действия в панели инструментов.

Еще одним полезным свойством класса ActivityDesigner, которое можно переопределить, является свойство Verbs. Оно позволяет добавить пункты в контекстное меню действия и используется конструктором ParallelActivity для вставки пункта Add Branch (Добавить ветвь) в контекстное меню действия, а также в меню Workflow (Рабочий поток). Также вы можете изменить список свойств, предоставляемых действием, переопределив метод конструктора PreFilterProperties, посредством которого размещаются параметры метода для CallExternalMethodActivity в таблице свойств. Если вам нужно внести расширение подобного рода в конструктор, вы должны запустить инструмент Lutz Roeder’s Reflector и загрузить сборки рабочего потока, чтобы увидеть, как Microsoft определяет некоторые из этих расширенных свойств.

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

ActivityToolboxItem и пиктограммы

Чтобы завершить разработку вашего пользовательского действия, вам нужно добавить пиктограмму и необязательно добавить класс, унаследованный от ActivityToolboxItem, который используется при отображении действия в панели инструментов Visual Studio.

Чтобы определить пиктограмму для действия, создайте изображение размером 16.16 пикселей и включите его в проект, после чего установите действие сборки для этого изображения в Embedded Resource. Это позволит включить изображение в манифест ресурсов сборки. Вы можете добавить в проект папку по имени Resources, как показано на рис. 41.11.


Рис. 41.11. Добавление в проект папки по имени Resources

Добавив файл изображения и установив его действие сборки в Embedded Resource, вы можете снабдить свое действие еще одним атрибутом, показанным в следующем фрагменте кода:

[ActivityValidator(typeof(WriteLineValidator))]
[Designer(typeof(WriteLineDesigner))]
[ToolboxBitmap(typeof(WriteLineActivity),"Resources.WriteLine.png")]
public class WriteLineActivity : Activity
{
...
}

Атрибут ToolboxBitmap имеет ряд конструкторов, и один из них, использованный здесь, принимает тип, определенный в сборке действия, а также имя ресурса. Когда вы добавляете ресурс в папку, его имя формируется из наименования пространства имен сборки и имени папки, в которой содержится изображение, так что полным квалифицированным именем нашего ресурса будет CustomActivities.Resources. WriteLine.png. Конструктор, используемый для атрибута ToolboxBitmap, прибавляет пространство имен, в котором находится тип-параметр, к строке, передаваемой в качестве второго аргумента, так что это все преобразуется в соответствующий ресурс при загрузке Visual Studio.

Последний класс, который потребуется создать, унаследован от ActivityToolboxItem. Этот класс используется при загрузке действия в панель инструментов Visual Studio. Типичное использование этого класса заключается в изменении отображаемого имени действия в панели инструментов — все встроенные действия имеют измененные имена, в которых из типа исключено слово “Activity”. С вашим классом вы можете сделать то же самое, установив свойство DisplayName в "WriteLine".

[Serializable]
public class WriteLineToolboxItem : ActivityToolboxItem
{
      /// <summary>
      /// Установить отображаемое имя WriteLine, то есть удалить из него строку 'Activity'
      /// </summary>
      /// <param name="t"></param>
      public WriteLineToolboxItem( Type t )
            : base( t )
      {
         base.DisplayName = "WriteLine";
      }

      /// <summary>
      /// Необходимо для среды времени проектирования Visual Studio
      /// </summary>
      /// <param name="info"></param>
      /// <param name="context"></param>
      private WriteLineToolboxItem( SerializationInfo info, StreamingContext context )
      {
         this.Deserialize( info, context );
      }
}

Класс унаследован от ActivityToolboxItem и переопределяет конструктор, чтобы изменить отображаемое имя; кроме того, он представляет конструктор для сериализации, используемый панелью инструментов при загрузке элемента в эту панель. Без конструктора вы получите ошибку при попытке добавить действие в панель инструментов. Обратите также внимание, что класс помечен как [Serializable].

Элемент панели инструментов добавляется к действию посредством использования атрибута ToolboxItem, как показано ниже:

ActivityValidator(typeof(WriteLineValidator))]
[Designer(typeof(WriteLineDesigner))]
[ToolboxBitmap(typeof(WriteLineActivity),"Resources.WriteLine.png")]
[ToolboxItem(typeof(WriteLineToolboxItem))]
public class WriteLineActivity : Activity
{
...
}

После внесения всех этих изменений вы можете скомпилировать сборку и затем создать новый проект рабочего потока. Чтобы добавить действие к панели инструментов, откройте рабочий поток и затем отобразите его контекстное меню, в котором выберите пункт Choose Items.

Затем вы сможете найти сборку, содержащую ваше действие, и когда добавите ее к панели инструментов, она будет выглядеть примерно так, как показано на рис. 41.12. Как видите, пиктограмма несколько меньше, чем нужно, но достаточно близко к тому.


Рис. 41.12. Добавленная пиктограмма

В следующем разделе мы еще вернемся к ActivityToolboxItem, когда будем говорить о пользовательских составных действиях, поскольку в этом классе есть еще несколько дополнительных средств, которые нужны только при добавлении составных действий на поверхность конструктора.

Пользовательские составные действия

Существуют два основных типа действий, унаследованные от Activity, которые можно трактовать как функции, вызываемые из рабочего потока. Действия же, унаследованные от CompositeActivity (такие как ParallelActivity, IfElseActivity и ListenActivity), являются контейнерами для других действий, и их поведение времени проектирования существенно отличается от поведения простых действий в том смысле, что они представляют область в конструкторе, куда можно помещать их дочерние действия.

В настоящем разделе мы создадим действие, которое можно назвать DaysOfWeekActivity. Это действие может быть использовано для выполнения разных частей рабочего потока на основе текущей даты. Например, вам может понадобиться инициировать выполнение рабочего потока по другому маршруту, обрабатывая заказы, которые поступили в выходные, в отличие от тех, что поступили в течение рабочей недели. Разбирая данный пример, вы познакомитесь с рядом усовершенствованных свойств рабочих потоков, и к концу раздела будете обладать хорошим пониманием того, как можно расширить систему за счет ваших собственных составных действий.

Для начала создадим пользовательское действие, имеющее свойство, по умолчанию установленное в текущую дату/время, и позволим устанавливать это свойство в другие значения, которые могут поступать от других действий рабочего потока либо в виде параметра, переданного рабочему потоку при его выполнении. Это составное действие будет содержать несколько ветвей — все они определяются пользователем.

Каждая из этих ветвей будет содержать перечислимую константу, определяющую, в какой день (или дни) ветвь должна выполняться. Ниже определяется действие и две ветви:

DaysOfWeekActivity
SequenceActivty: Monday, Tuesday, Wednesday, Thursday, Friday
<other activites as appropriate>
SequenceActivity: Saturday, Sunday
<other activites as appropriate>

Для данного примера вам понадобится перечисление, определяющее дни недели, и оно должно включать атрибут [Flags] (поэтому вы не можете использовать встроенное перечисление DayOfWeek, определенное в пространстве имен System, поскольку оно не имеет атрибута [Flags]).

[Flags]
[Editor(typeof(FlagsEnumEditor), typeof(UITypeEditor))]
public enum WeekdayEnum : byte
{
   None = 0x00,
   Sunday = 0x01,
   Monday = 0x02,
   Tuesday = 0x04,
   Wednesday = 0x08,
   Thursday = 0x10,
   Friday = 0x20,
   Saturday = 0x40
}

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

Определив тип перечисления, вы можете в качестве начальной заглушки назначить само действие. Пользовательские составные действия обычно наследуются от класса CompositeActivity, который помимо прочего, определяет свойство Activities, представляющее собой коллекцию всех составляющих его действий. Свойство DateTime, используемое при исполнении действия, также присутствует.

public class DaysOfWeekActivity : CompositeActivity
{
/// <summary>
/// Методы get/set свойства дня недели
/// </summary>
[Browsable(true)]
[Category("Behavior")]
[Description("Привязано к свойству DateTime, укажите дату и время
              или оставьте пустым для DateTime.Now")]
[DefaultValue(typeof(DateTime),"")]
public DateTime Date
{
   get { return (DateTime)base.GetValue(DaysOfWeekActivity.DateProperty); }
   set { base.SetValue(DaysOfWeekActivity.DateProperty, value); }
}
/// <summary>
/// Регистрация свойства DayOfWeek
/// </summary>
public static DependencyProperty DateProperty =
   DependencyProperty.Register("Date", typeof(DateTime),
   typeof(DaysOfWeekActivity));
}

Свойство Date предоставляет обычные средства доступа get и set, а кроме этого добавлен ряд стандартных атрибутов, чтобы они корректно отображались в панели свойств. Поэтому код выглядит несколько иначе, чем у обычного свойства .NET, поскольку эти средства get и set не используют стандартное поле для хранения значений, а вместо этого применяется то, что носит название DependencyProperty (свойство зависимости).

Класс Activity (а, следовательно, и данный класс, поскольку он обязательно порожден от Activity) унаследован от класса DependencyObject, и это определяет словарь ключевых значений DependencyProperty. Это косвенное получение и установка значений свойства применяется WF для поддержки привязки, то есть связывания свойства одного действия со свойством другого. В качестве примера принято передавать параметры по коду, иногда по значению, иногда по ссылке. WF использует привязку для связывания вместе свойств, поэтому в настоящем примере вы можете иметь свойство DateTime, определенное в рабочем потоке, а действие может быть привязано к значению во время выполнения. Позднее в этой главе будет показанпример такой привязки.

Если вы соберете это действие, оно будет делать не так много — на самом деле оно даже не позволит помещать в него дочерние действия, поскольку вы не определили класс конструктора для этого действия.

Добавление класса Designer

Как вы видели в примере с WriteLineActivity ранее в этой главе, каждое действие может иметь ассоциированный с ним класс Designer, используемый для изменения поведения действия во время проектирования. Вы видели пустой Designer в WriteLineActivity, но для составного действия придется переопределить несколько методов, чтобы добавить возможность обработки специальных случаев.

public class DaysOfWeekDesigner : ParallelActivityDesigner
{
      public override bool CanInsertActivities
      ( HitTestInfo insertLocation, ReadOnlyCollection<Activity> activities )
      {
         foreach ( Activity act in activities )
         {
            if ( !( act is SequenceActivity ) )
               return false;
         }

         return base.CanInsertActivities( insertLocation, activitiesToInsert );
      }

      protected override CompositeActivity OnCreateNewBranch()
      {
         return new SequenceActivity();
      }
}

Этот Designer наследуется от ParallalActivityDesigner, который предоставляет в ваше распоряжение поведение времени проектирования при добавлении дочерних действий. Вам придется переопределить CanInsertActivities для возврата false, когда любое из добавленных действий не будет являться SequenceActivity. Если все добавляемые действия будут соответствующего типа, то вы сможете вызвать метод базового класса, который выполнит некоторые дополнительные проверки над типами действий, допустимыми внутри вашего пользовательского составного действия.

Также вы должны переопределить метод OnCreateNewBranch, вызываемый, когда пользователь выбирает пункт меню Add Branch. Designer ассоциируется с действием посредством применения атрибута [Designer], как показано ниже:

[Designer(typeof(DaysOfWeekDesigner))]
public class DaysOfWeekActivity : CompositeActivity
{}

Поведение времени проектирования почти готово; однако в это действие также нужно добавить класс, унаследованный от ActivityToolboxItem, который определит то, что случится, когда экземпляр этого действия будет перетаскиваться из панели инструментов. Поведение по умолчанию состоит просто в конструировании нового действия; тем не менее, в нашем примере вам понадобится сразу создать две ветви по умолчанию. Ниже приведен класс элемента панели инструментов, который сделает это.

[Serializable]
public class DaysOfWeekToolboxItem : ActivityToolboxItem
{
      public DaysOfWeekToolboxItem( Type t )
            : base( t )
      {
         this.DisplayName = "DaysOfWeek";
      }

      private DaysOfWeekToolboxItem( SerializationInfo info, StreamingContext context )
      {
         this.Deserialize( info, context );
      }

      protected override IComponent[] CreateComponentsCore( IDesignerHost host )
      {
         CompositeActivity parent = new DaysOfWeekActivity();
         parent.Activities.Add( new SequenceActivity() );
         parent.Activities.Add( new SequenceActivity() );
         return new IComponent[] { parent };
      }
}

Как видно в этом коде, отображаемое имя действия изменено, реализован конструктор сериализации и переопределен метод CreateComponentsCore.

Этот метод вызывается в конце операции перетаскивания, и именно здесь вы конструируете экземпляр DaysOfWeekActivity. В коде вы также строите два дочерних последовательных действия, поскольку это сразу облегчит работу пользователю во время проектирования. То же самое осуществляют некоторые встроенные действия. Так, например, когда вы помещаете действие IfElseActivity на поверхность конструктора, его класс панели инструментов также добавляет сразу две ветви. Подобная вещь случается также, когда вы добавляете в свой рабочий поток действие ParallelActivity.

Конструктор сериализации и атрибут [Serializable] необходимы всем классам, унаследованным от ActivityToolboxItem.

Последнее, что потребуется сделать — это ассоциировать этот класс элемента панели инструментов с действием:

[Designer(typeof(DaysOfWeekDesigner))]
[ToolboxItem(typeof(DaysOfWeekToolboxItem))]
public class DaysOfWeekActivity : CompositeActivity
{}

Теперь пользовательский интерфейс вашего действия почти завершен, как видно на рис. 41.13.


Рис. 41.13. Добавление Designer к пользовательскому действию

Теперь нужно определить свойства каждого из последовательных действий, показанных на рис. 41.13, чтобы пользователь смог определить, в какие дни какая из них должна выполняться. Есть два способа сделать это в Windows Workflow: вы можете определить подкласс SequenceActivity и определить все это в нем, или же можно воспользоваться другим средством свойств зависимости, называемым Attached Properties (прикрепленные свойства).

Мы предлагаем применить второй метод, который исключит необходимость в подклассе, но вместо этого позволит эффективно расширить последовательное действие без потребности в его исходном коде.

Прикрепленные свойства

При регистрации свойств зависимости вы можете вызвать метод RegisterAttached для создания прикрепленного свойства. Прикрепленное свойство — это такое свойство, которое определено в одном классе, но отображается в другом. В данном случае вы определяете свойство в DaysOfWeekActivity, но на самом деле это свойство отображается в пользовательском интерфейсе как прикрепленное к последовательному действию.

public static DependencyProperty
   WeekdayProperty =
      DependencyProperty.RegisterAttached("Weekday",
         typeof(WeekdayEnum), typeof(DaysOfWeekActivity),
      new PropertyMetadata(DependencyPropertyOptions.Metadata));

Последняя строка позволяет специфицировать дополнительную информацию о свойстве; в данном примере специфицировано, что этим свойством будет Metadata.

Свойства Metadata отличаются от обычных свойств в том отношении, что они эффективно читаются только во время выполнения. Вы можете воспринимать свойство Metadata как нечто подобное объявлению констант внутри C#. Вы не можете изменить константы во время выполнения программы, и точно также вы не можете изменять свойства Metadata во время выполнения рабочего потока.

В данном примере вы хотите определить дни активизации действия, поэтому вы можете установить в Designer это поле в “Saturday, Sunday”. В коде, выданном для рабочего потока, вы можете увидеть объявления, подобные показанным ниже.

this.sequenceActivity1.SetValue
   (DaysOfWeekActivity.WeekdayProperty,
      ((WeekdayEnum)((WeekdayEnum.Sunday | WeekdayEnum.Saturday))));

В дополнение к определению свойства зависимости вам понадобятся методы для получения и установки этого значения для произвольного действия. Это обычно определяется как статический метод составного действия и показано в следующем коде:

public static void SetWeekday( Activity activity, object value )
{
   if ( null == activity )
      throw new ArgumentNullException( "activity" );

   if ( null == value )
      throw new ArgumentNullException( "value" );

   activity.SetValue( DaysOfWeekActivity.WeekdayProperty, value );
}

public static object GetWeekday( Activity activity )
{
   if ( null == activity )
      throw new ArgumentNullException( "activity" );

   return activity.GetValue( DaysOfWeekActivity.WeekdayProperty );
}

Есть еще два изменения, которые вы должны внести, чтобы дополнительное свойство могло отображаться как присоединенное к SequenceActivity. Первое — это создать поставщика расширителей, который сообщит Visual Studio о необходимости включения дополнительного свойства в последовательное действие, и второе — зарегистрировать этого поставщика, что делается переопределением метода Initialize в Activity Designer и добавлением следующего кода к нему:

protected override void Initialize( Activity activity )
{
   base.Initialize( activity );
   IExtenderListService iels = base.GetService( typeof( IExtenderListService ) )
                               as IExtenderListService;

   if ( null != iels )
   {
      bool extenderExists = false;
      foreach ( IExtenderProvider provider in iels.GetExtenderProviders() )
      {
         if ( provider.GetType() == typeof( WeekdayExtenderProvider ) )
         {
            extenderExists = true;
            break;
         }
      }

      if ( !extenderExists )
      {
         IExtenderProviderService ieps =
            base.GetService( typeof( IExtenderProviderService ) )
            as IExtenderProviderService;

         if ( null != ieps )
            ieps.AddExtenderProvider( new WeekdayExtenderProvider() );
      }
   }
}

Вызовы GetService в приведенном коде предназначены для того, чтобы позволить пользовательскому конструктору запросить службы, предоставляемыехостом (в данном случае им является Visual Studio). Вы запрашиваете у Visual Studio IextenderListService, который предоставляет способ перечислить всех доступных поставщиков расширителей, и если не будет найдено ни одного экземпляра WeekdayExtenderProvider, тогда запрашивается IExtenderProviderService, и добавляется новый поставщик. Ниже показан код поставщика расширителей.

[ProvideProperty("Weekday", typeof(SequenceActivity))]
public class WeekdayExtenderProvider : IExtenderProvider
{
   bool IExtenderProvider.CanExtend(object extendee)
   {
      bool canExtend = false;
      if ((this != extendee) && (extendee is SequenceActivity))
      {
         Activity parent = ((Activity)extendee).Parent;
         if (null != parent)
            canExtend = parent is DaysOfWeekActivity;
      }
      return canExtend;
   }
   public WeekdayEnum GetWeekday(Activity activity)
   {
      WeekdayEnum weekday = WeekdayEnum.None;
      Activity parent = activity.Parent;
      if ((null != parent) && (parent is DaysOfWeekActivity))
         weekday = (WeekdayEnum)DaysOfWeekActivity.GetWeekday(activity);
      return weekday;
   }
   public void SetWeekday(Activity activity, WeekdayEnum weekday)
   {
      Activity parent = activity.Parent;
      if ((null != parent) && (parent is DaysOfWeekActivity))
         DaysOfWeekActivity.SetWeekday(activity, weekday);
   }
}

Поставщик расширителей снабжен свойствами, которые он предоставляет, и для каждого из этих свойств он должен обеспечить общедоступные методы Get<Property> и Set<Property>. Имена этих методов должны соответствовать именам свойства с соответствующим префиксом Get или Set.

После внесения показанных выше изменений в Designer и добавления поставщика расширений, когда вы щелкнете на последовательном действии в визуальном конструкторе, то в Visual Studio увидите свойства, показанные на рис. 41.14.


Рис. 41.14. Прикрепленные свойства

Поставщики расширителей используются в .NET и для других целей. Одним из распространенных применений является добавление всплывающих подсказок (tooltips) к элементам управления в проекте Windows Forms. Когда вы добавляете в форму элемент-подсказку, это регистрирует расширитель и добавляет свойство Tooltip к каждому элементу управления в форме.

Рабочие потоки

До этого момента в настоящей главе внимание концентрировалось на действиях, но не говорилось о рабочих потоках. Рабочий поток — это просто список действий, и на самом деле рабочий поток сам по себе является еще одним типом действий. Применение такой модели упрощает механизм исполняющей системы, поскольку ему достаточно знать, как следует выполнять только один тип объектов — любой, унаследованный от класса Activity.

Каждый экземпляр рабочего потока уникально идентифицирован свойством InstanceId, то есть Guid, который может быть назначен исполняющей системой, или же этот Guid может быть представлен исполняющей системе вашим кодом — обычно это делается для корреляции работающего экземпляра рабочего потока с некоторым внешним источником данных, находящимся вне рабочего потока, таким как строка базы данных. Вы можете обратиться к определенному экземпляру рабочего потока посредством метода GetWorkflow(Guid) класса WorkflowRuntime.

Существуют два типа рабочих потоков, доступных в WF — последовательный и конечный автомат.

Последовательные рабочие потоки

Корневое действие последовательного рабочего потока — это SequentialWorkflowActivity. Этот класс наследован от SequenceActivity, который вы уже видели, и он определяет два события, к которым при необходимости вы можете присоединить обработчики. Эти события — Initialized и Completed.

Последовательный рабочий поток начинает работу, выполняя находящееся в нем первое дочернее действие, и обычно продолжается до тех пор, пока не выполнит все остальные дочерние действия. Есть несколько ситуаций, когда рабочий поток не выполняет всех своих действий. Одна — это когда возникает исключение в процессе выполнения рабочего потока, а другая — когда внутри потока встречается TerminateActivity.

Рабочий поток может и не выполняться постоянно; например, когда встречается DelayActivity, то поток входит в состояние ожидания, и тогда он может быть удален из памяти, если определена служба постоянства потоков. Постоянство потоков описано далее в разделе “Служба постоянства”.

Рабочие потоки типа конечных автоматов

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

Примером может служить рабочий поток, используемый для управления зданием. В этом случае вы можете моделировать класс двери, которая может быть закрыта или открыта, и класс замка, который может быть замкнут или незамкнут. Изначально, когда вы загружаете систему (или здание!), вы начинаете с известного состояния; для простоты предположим, что все двери закрыты и заперты на замок, так что состояние данной двери — “закрыта и заперта”.

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

Из этого состояния есть два потенциальных пути — сотрудник открывает дверь (вы знаете это, поскольку дверь снабжена сенсором открытия/закрытия), или же он решит не входить, поскольку забыл что-то у себя в машине, и тогда после небольшой паузы вы запираете дверь. Таким образом, дверь может перейти в свое состояние “закрыта и заперта” или же в состояние “открыта и не заперта”.

Теперь предположим, что сотрудник вошел в здание и закрыл за собой дверь; опять же вы должны выполнить переход из состояния “открыта и не заперта” в состояние “закрыта и не заперта”, и после паузы должен произойти переход в состояние “закрыта и заперта”. Вы также можете выдать сигнал тревоги, если дверь осталась “открытой и не запертой” в течение длительного периода времени.

Смоделировать эту ситуацию в среде Windows Workflow необыкновенно просто. Вам нужно определить состояния, которые может принимать система, и затем определить события, которые могут переводить ее из одного состояния в другое. В табл. 41.2 описаны состояния системы и подробности переходов, которые возможны из каждого состояния, а также ввод (как внешний, так и внутренний), изменяющий эти состояния.

Одно из других средств, которые вы можете решить добавить в систему — это способность реагировать на сигнал пожарной тревоги. Когда сигнал тревоги будет выключен, вам нужно отпереть все двери, чтобы все могли покинуть здание, а пожарная команда — проникнуть в него. Вы можете смоделировать это как финальное состояние рабочего потока управляющего дверью, поскольку из этого состояния вся система должна быть сброшена, как только пожарная тревога будет отменена.

СостояниеПереход
Закрыта и запертаЭто начальное состояние системы.
В ответ на вставку пользователем карточки (и успешной проверки права доступа) состояние изменяется в “закрыта и не заперта”, то есть замок двери отпирается электроникой
Закрыта и не запертаКогда дверь находится в этом состоянии, можетпроизойти одно из двух событий:
(1) пользователь открывает дверь — выполняется переход в состояние “открыта и не заперта”;
(2) истекает время таймера и дверь возвращается в состояние “закрыта и заперта”
Открыта и не запертаИз этого состояния рабочий поток может перейти только в состояние “закрыта и не заперта”
Пожарная тревогаЭто состояние — финальное для рабочего потока, и в него можно перейти из любого другого состояния
Таблица 41.2. Состояния и переходы системы

Рабочий поток на рис. 41.15 определяет конечный автомат и показывает состояния, которые он может принимать, а линии означают возможные переходы из одного состояния в другое.


Рис. 41.15. Определение конечного автомата

Начальное состояние рабочего потока смоделировано действием ClosedLocked. Оно состоит из некоторого кода инициализации (запирающего дверь) и затем действия на основе события, которое ожидает внешнего события — в данном случае, ввода сотрудником собственного кода доступа в здание. Каждое из показанных действий внутри фигуры состояния состоит из последовательных рабочих потоков — таким образом, был определен рабочий поток инициализации системы (CLInitialize) и рабочий поток, отвечающий на внешние события, возникающие при воде сотрудником его PIN-кода (RequestEntry). Взгляните на определение рабочего потока RequestEntry, как он изображен на рис. 41.16.


Рис. 41.16. Определение рабочего потока RequestEntry

Каждое состояние состоит из ряда подпотоков, каждый из которых имеет управляемое событием действие в начале, а затем некоторое количество других действий, которые формируют обрабатывающий код внутри состояния. На рис. 41.16 имеется действие HandleExternalEventActivity в начале, которое ожидает ввода PIN. Затем выполняется его проверка, и если код действителен, рабочий поток переходит в состояние ClosedUnlocked.

Состояние ClosedUnlocked состоит из двух рабочих потоков — один реагирует на событие открытия двери, которое переводит рабочий поток в состояние OpenUnlocked, а другой, который содержит действие задержки, используется для перехода в состояние ClosedLocked. Действие, управляемое состоянием, работает в манере, аналогичной действию ListenActivity, показанному ранее в этой главе — это состояние состоит из ряда управляемых событиями рабочих потоков, и любое возникающее событие обрабатывается только одним рабочим потоком.

Для поддержки рабочего потока вы должны быть готовы инициировать события в системе, чтобы вызвать изменения состояния системы. Это делается за счет использования интерфейса и реализации этого интерфейса, и такая пара объектов называется внешней службой (external service). Ниже в этой главе будет описан интерфейс, используемый в данном конечном автомате.

Передача параметров рабочему потоку

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

Механизм передачи параметров для рабочих потоков несколько отличается от того же механизма в стандартных классах .NET, где обычно вы передаете параметры при вызове метода. Для рабочего потока вы передаете параметры, сохраняя их в словаре в виде пар “имя-значение”, и когда вы конструируете рабочий поток, то проходите по этому словарю. Когда WF планирует на выполнение рабочий поток, эти пары “имя-значение” используются для установки значений общедоступных свойств экземпляра рабочего потока. Каждое имя параметра проверяется по общедоступным свойствам рабочего потока и, если обнаружено соответствие, вызывается средство доступа set этого свойства и ему передается значение параметра. Если вы добавляете в словарь пару “имязначение”, причем имя не соответствует свойству рабочего потока, то при попытке сконструировать такой рабочий поток будет возбуждено исключение.

В качестве примера рассмотрим следующий рабочий поток, определяющий целочисленное свойство OrderID:

public class OrderProcessingWorkflow: SequentialWorkflowActivity
{
   public int OrderID
   {
      get { return _orderID; }
      set { _orderID = value; }
   }
   private int _orderID;
}

Следующий фрагмент показывает, как можно передать параметр — идентификатор заказа экземпляру этого рабочего потока:

WorkflowRuntime runtime = new WorkflowRuntime ();
Dictionary<string,object> parms = new Dictionary<string,object>();
parms.Add("OrderID", 12345) ;
WorkflowInstance instance =
   runtime.CreateWorkflow(typeof(OrderProcessingWorkflow), parms);
instance.Start();
... //Прочий код

В приведенном примере конструируется словарь Dictionary<string, object>, содержащий параметры, которые вы хотите передать рабочему потоку, и затем использовать при его конструировании. Приведенный выше код включает классы WorkflowRuntime и WorkflowInstance, которые еще не были описаны;о них речь пойдет в разделе “Хостинг рабочих потоков” далее в главе.

Возврат результатов из рабочего потока

Другим распространенным требованием, предъявляемым к рабочему потоку, является возврат выходных параметров — может быть, тех, что будут как-то использованы, а затем записаны в базу данных или другое постоянное хранилище.

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

Поэтому чтобы получить выходные параметры рабочего потока, вы должны предусмотреть обработчик события WorkflowCompleted, и этот обработчик может извлекать выходные параметры из рабочего потока. В следующем коде показан пример того, как это можно сделать.

using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
   AutoResetEvent waitHandle = new AutoResetEvent(false);
   workflowRuntime.WorkflowCompleted +=
      delegate(object sender, WorkflowCompletedEventArgs e)
      {
         waitHandle.Set();
         foreach (KeyValuePair<string, object> parm in e.OutputParameters)
         {
            Console.WriteLine("{0} = {1}", parm.Key, parm.Value);
         }
      };
   WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Workflow1));
   instance.Start();
   waitHandle.WaitOne();
}

Здесь мы присоединили делегат к событию WorkflowCompleted. Внутри него осуществляется проход по коллекции OutputParameters экземпляра класса WorkflowCompletedEventArgs, переданного делегату, и все выходные параметры отображаются на консоли. Эта коллекция содержит все общедоступные свойства рабочего потока. Нет никаких пометок специфических выходных параметров рабочего потока.

Привязка параметров к действиям

Теперь, когда вы узнали, как передавать параметры в рабочий поток, вам также понадобится посмотреть, как связать эти параметры с действиями. Это делается с помощью механизма, называемого привязкой (binding). В ранее определенном DaysOfWeekActivity имеется свойство Date, которое, как упоминалось ранее, может быть жестко закодировано либо привязано с другому значению внутри рабочего потока. Привязываемое свойство отображается в таблице свойств, в среде Visual Studio, как показано на рис. 41.17. Наличие пиктограммы синего цвета рядом с именем свойства говорит о том, что это привязываемое свойство.


Рис. 41.17. Привязываемое свойство

Двойной щелчок на пиктограмме привязкивызовет диалог, показанный на рис. 41.18. Этот диалог позволит выбрать соответствующее свойство для привязки свойства Date.

На рис. 41.18 выбрано свойство OrderDate рабочего потока (которое определено как обычное свойство .NET, как было показано ранее в одном из предыдущих фрагментов кода). Всякое привязываемое свойство может быть привязано либо к свойству рабочего потока, внутри которого определено действие, либо к свойству любого действия, относящегося к рабочему потоку, которое находится выше текущего действия.


Рис. 41.18. Диалоговое окно, отображаемое в результате двойного щелчка на пиктограмме привязки

Обратите внимание, что тип данных привязываемого свойства должен соответствовать типу данных того свойства, к которому осуществляется привязка — диалог не позволит вам связать несоответствующие типы.

Мы повторим код свойства Date, чтобы показать, как работает привязка, а объяснения вы найдете в следующем абзаце.

public DateTime Date
{
   get { return (DateTime)base.GetValue(DaysOfWeekActivity.DateProperty); }
   set { base.SetValue(DaysOfWeekActivity.DateProperty, value); }
}

Когда вы привязываете свойство в рабочем потоке, то “за кулисами” конструируется объект типа ActivityBind, и это и будет тем “значением”, которое сохраняется внутри свойства зависимости. Поэтому средству set свойства будет передан объект типа ActivityBind, и он будет сохранен внутри словаря свойств для данного действия. Объект ActivityBind состоит из данных, описывающих действие, к которому выполняется привязка, и свойства данного действия, которое будет использовано во время выполнения.

При чтении значения свойства вызывается метод GetValue объекта DependencyObject, и этот метод проверяет значение лежащего в основе свойства на предмет того, является ли оно объектом ActivityBind. Если это так, затем разрешается действие, к которому выполняется привязка, а затем читается реальное значение свойства из этого действия. Однако если привязанное значение имеет другой тип, тогда просто возвращается объект из метода GetValue().

Исполняющая среда рабочего потока

Для того чтобы запустить рабочий поток, необходимо создать экземпляр класса WorkflowRuntime. Обычно это делается один раз внутри вашего приложения, и этот объект обычно определен как статический член приложения, поэтому он доступен в приложении отовсюду.

Когда вы запускаете исполняющую систему, она перезагружает все экземпляры рабочих потоков, которые существовали на момент последнего запуска приложения, посредством чтения этих экземпляров из постоянного хранилища. Здесь используется служба, называемая службой постоянства (persistence service), которую мы определим ниже.

Исполняющая система содержит методы для конструирования экземпляров рабочих потоков — имеются шесть разных методов CreateWorkflow, которые могут быть использованы для этого, а кроме этого, исполняющая система также содержит методы для перезагрузки экземпляров и перечисления всех работающих экземпляров.

Исполняющая система также поддерживает ряд событий, которые возбуждаются на протяжении жизни рабочего потока, такие как WorkflowCreated (возбуждается, когда конструируется новый экземпляр рабочего потока), WorkflowIdled (возбуждается, когда рабочий поток ожидает ввода, как в приведенном ранее примере обработки отчетов о расходах) и WorkflowCompleted (возбуждается при завершении рабочего потока).

Службы рабочих потоков

Рабочий поток не существует сам по себе. Как показано в предыдущих разделах, рабочий поток выполняется внутри WorkflowRuntime, и эта исполняющая система предоставляет службы запущенным рабочим потокам.

Служба — это любой класс, который может понадобиться во время выполнения рабочего потока. Есть некоторые стандартные службы, предоставляемые вашим потокам исполняющей системой, и вы по желанию можете конструировать свои собственные службы, которые будут применяться запущенными рабочими потоками.

В настоящем разделе описываются две стандартных службы, предоставляемые исполняющей системой, и затем будет показано, как создавать свои собственные службы и некоторые экземпляры, когда это необходимо.

Когда выполняется действие, ему передается некоторая контекстная информация в параметре ActivityExecutionStatus метода Execute.

protected override ActivityExecutionStatus Execute
(ActivityExecutionContext executionContext)
{
...
}

Одним из доступных методов в этом контекстном параметре является GetService<T>. Как показано в следующем коде, он может использоваться для доступа к службе, прикрепленной к исполняющей среде рабочего потока.

protected override ActivityExecutionStatus Execute
(ActivityExecutionContext executionContext)
{
    ICustomService myService = executionContext.GetService<ICustomService>();
... //Делать что-нибудь со службой
}

Службы, развернутые в исполняющей среде, добавляются в нее до вызова метода StartRuntime; если вы попытаетесь добавить службу к исполняющей среде после ее запуска, то получите исключение.

Для добавления служб к исполняющей среде предусмотрены два метода: вы можете конструировать службы в коде и затем добавлять их к исполняющей среде вызовом метода AddService, либо вы можете определить службы внутри конфигурационного файла приложения, и они будут автоматически сконструированы и добавлены к исполняющей среде.

В следующем фрагменте кода показано, как добавлять службы во время выполнения в коде. Добавляемые службы описаны ниже в этом разделе.

using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
   workflowRuntime.AddService(
      new SqlWorkflowPersistenceService(conn, true, new TimeSpan(1,0,0),
      new TimeSpan(0,10,0)));
   workflowRuntime.AddService(new SqlTrackingService(conn));
...
}

Здесь конструируются экземпляр SqlWorkflowPersistenceService, который используется исполняющей средой для сохранения состояния рабочего потока, а также экземпляр SqlTrackingService, который записывает события, происходящие в рабочем потоке во время его работы.

Чтобы создать службы с помощью конфигурационного файла приложения, нужно добавить соответствующий раздел-обработчик для исполняющей среды рабочего потока, после чего добавить службы в этот раздел:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <section name="WF"
      type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection,
      System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
      PublicKeyToken=31bf3856ad364e35" />
   </configSections>
   <WF Name="Hosting">
      <CommonParameters/>
      <Services>
         <add type="System.Workflow.Runtime.Hosting.
                    SqlWorkflowPersistenceService,
                    System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
                    PublicKeyToken=31bf3856ad364e35"
              connectionString="Initial Catalog=WF;Data Source=.;
                                Integrated Security=SSPI;"
              UnloadOnIdle="true"
              LoadIntervalSeconds="2"/>
         <add type="System.Workflow.Runtime.Tracking.SqlTrackingService,
                    System.Workflow.Runtime, Version=3.0.00000.0,
                    Culture=neutral,
                    PublicKeyToken=31bf3856ad364e35"
              connectionString="Initial Catalog=WF;Data Source=.;
                                Integrated Security=SSPI;"
              UseDefaultProfile="true"/>
      </Services>
   </WF>
</configuration>

Внутри конфигурационного файла вы добавляете обработчик секции WF (имя не имеет значения, но должно совпадать с именем, приведенным ниже в секции конфигурации), а затем создаете соответствующие элементы для этого раздела. Элемент <Services> может содержать произвольный список элементов, состоящих из типа .NET и параметров, передаваемых службе при ее конструировании исполняющей средой. Для чтения настроек из файла конфигурации приложения во время выполнения вызывается другой конструктор, как показано ниже:

using(WorkflowRuntime workflowRuntime = new WorkflowRuntime("WF"))
{
...
}

Этот конструктор создает экземпляры каждой службы, определенной внутри конфигурационного файла, и добавляет их в коллекцию служб во время выполнения. В следующем разделе рассматриваются некоторые стандартные службы, доступные в WF.

Служба постоянства

Когда рабочий поток выполняется, он может достичь состояния ожидания — такое может случиться, когда выполняетсядействие ожидания или когда вы ждете внешнего ввода в действии прослушивания. В этот момент говорят, что рабочий поток простаивает (idle), и потому является кандидатом на сохранение.

Предположим, что вы начали выполнение 1000 рабочих потоков на сервере, и каждый из этой тысячи потоков попал в состояние простоя. В этот момент нет необходимости поддерживать данные всех этих экземпляров в памяти, так что в идеале было бы иметь возможность выгрузить рабочий поток и освободить ресурсы для других задач. Служба постоянства (persistence service) предназначена специально для этого.

Когда рабочий поток достигает состояния простоя, исполняющая среда проверяет существование службы, унаследованной от класса WorkflowPersistenceService. Если такая служба существует, ей передается экземпляр рабочего потока, и она затем может зафиксировать текущее состояние рабочего потока и сохранить его в постоянном хранилище. Вы можете сохранить состояние рабочего потока на диске или в файле либо в базе данных, такой как SQL Server.

Библиотеки рабочих потоков содержат реализацию службы постоянства, которая сохраняет данные в базе SQL Server — это SqlWorkflowPersistenceService. Для того чтобы использовать упомянутую службу, вам нужно запустить два сценария на экземпляре SQL Server: один из них конструирует схему, а другой создает хранимые процедуры, используемые службой постоянства. Эти сценарии по умолчанию расположены в каталоге C:\Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN.

Сценарии для выполнения в базе данных называются SqlPersistenceService_ Schema.sql и SqlPersistenceService_Logic.sql. Они должны запускаться по порядку, сначала — файл схемы, затем — файл логики. Схема для службы постоянства SQL содержит две таблицы: InstanceState и CompletedScope; это, по сути, закрытые таблицы, которые не предназначены для использования вне службы постоянства SQL.

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

Служба постоянства SQL может быть использована несколькими исполняющими средами одновременно — она реализует механизм блокировки, так что рабочий поток доступен только одному экземпляру исполняющей среды за раз. Когда имеется несколько серверов, и все они выполняют рабочие потоки, взаимодействующие с одним и тем же постоянным хранилищем, это блокирующее поведение становится несущественным.

Чтобы увидеть, что именно добавляется в постоянное хранилище, сконструируйте новый проект рабочего потока и добавьте к исполняющей среде экземпляр SqlWorkflowPersistenceService. Ниже приведен пример применения декларативного кода:

using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
   workflowRuntime.AddService(
      new SqlWorkflowPersistenceService(conn, true, new TimeSpan(1,0,0),
      new TimeSpan(0,10,0)));
// Здесь выполняется рабочий поток...
}

Если вы конструируете рабочий поток, содержащий DelayActivity, и устанавливаете величину задержки около 10 секунд, то затем вы можете увидеть его данные в таблице InstanceState. Параметры, передаваемые конструктору службы постоянства, перечислены в табл. 41.3.

ПараметрОписаниеЗначение по умолчанию
ConnectionStringСтрока подключения к базе данных, используемой службой постоянстваНет
UnloadOnIdleОпределяет, будет ли рабочий поток выгружен во время простоя. Всегда следует устанавливать в true, иначе никакого постоянного хранения не произойдетFalse
InstanceOwnershipDurationОпределяет длительность периода времени, когда экземпляр рабочего потока будет принадлежать исполняющей системе, загрузившей этот потокНет
LoadingIntervalИнтервал, используемый для обновления в базе данных записей постоянного хранения2 минуты
Таблица 41.3. Параметры, передаваемые конструктору службы постоянства

Служба отслеживания

Во время выполнения рабочего потока может возникнуть необходимость записать, какие действия уже были выполнены, и в случае составного действия наподобие IfElseActivity или ListenActivity — какая именно ветвь исполняется. Эти данные могут применяться как протокол аудита для экземпляра рабочего потока, который позднее можно просмотреть на предмет того, какие действия были выполнены и какие данные использовались в рабочем потоке. Для такого рода протоколирования может применяться служба отслеживания (tracking service), и при необходимости ее можно настроить на фиксацию как можно большего или как можно меньшего объема информации о выполняющемся рабочем потоке.

Как принято в WF, служба отслеживания реализована в виде абстрактного базового класса по имени TrackingService, так что очень просто заменить стандартную реализацию отслеживания вашей собственной. Есть одна конкретная реализация службы отслеживания, доступная в сборках Windows Workflow — это SqlTrackingService.

Чтобы записать данные о состоянии рабочего потока, необходимо определить TrackingProfile. Этот объект определяет, какие именно события должны быть записаны, так что вы можете, к примеру, фиксировать только старт и финиш рабочего потока, пропуская все прочие данные, поступающие от выполняющегося экземпляра. Более типичный случай — протоколирование всех событий рабочего потока и каждого действия в нем, что позволяет зафиксировать полную картину профиля выполнения рабочего потока.

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

Класс профиля отслеживания показан ниже на рис. 41.19. Этот класс содержит свойства-коллекции для точек отслеживания действий, пользователя и рабочего потока в целом. Точка отслеживания — это объект (вроде WorkflowTrackPoint), который обычно определяет место соответствия (mach location) и некоторые дополнительные данные для записи


Рис. 41.19. Класс профиля отслеживания

при попадании в эту точку отслеживания. Место соответствия определяет, когда точка отслеживания действительна, поэтому, например, вы можете определить WorkflowTrackPoint, в которой будет выполняться запись некоторых данных при создании рабочего потока, а другую — для записи тех же данных при завершении рабочего потока.

После того как данные записаны, может оказаться необходимым отобразить путь выполнения рабочего потока, например, как показано на рис. 41.20. Здесь можно видеть выполненный рабочий поток, причем каждое его запущенное действие помечено специальным значком — птичкой. Данные для этого отображения читаются из хранилища информации об отслеживании данного экземпляра рабочего потока.

Для того чтобы прочесть данные, сохраненные посредством SqlTrackingService, вы можете напрямую отправить запросы базе данных SQL, однако Microsoft предлагает для этого класс SqlTrackingQuery, определенный в пространстве имен System.Workflow. Runtime.Tracking. В приведенном ниже примере кода показано, как извлечь список рабочих потоков, которые отслеживались между двумя датами:

public IList<SqlTrackingWorkflowInstance> GetWorkflows
   (DateTime startDate, DateTime endDate, string connectionString)
{
   SqlTrackingQuery query = new SqlTrackingQuery (connectionString);
   SqlTrackingQueryOptions queryOptions = new SqlTrackingQueryOptions();
   query.StatusMinDateTime = startDate;
   query.StatusMaxDateTime = endDate;
   return (query.GetWorkflows (queryOptions));
}

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

На рис. 41.20 видно, что все действия потока были выполнены — так может быть не всегда, если рабочий поток все еще выполняется, или если некоторые решения, принятые внутри потока, привели к выбору разных путей выполнения. Данные отслеживания содержат такие подробности, как то, какие действия были исполнены, и эта информация может быть сравнена с общим списком действий, чтобы сгенерировать картинку вроде показанной на рис. 41.20. Можно также извлечь данные из рабочего потока во время выполнения, которые могут быть использованы для формирования протокола аудита выполнения этого рабочего потока.


Рис. 41.20. Путь выполнения рабочего потока

Пользовательские службы

В дополнение к встроенным службам, таким как служба постоянства и служба отслеживания, вы также можете добавлять свои собственные объекты в коллекцию служб, поддерживаемую WorkflowRuntime. Эти службы обычно определяются с применением интерфейса и реализации, так что вы можете легко заменить одну службу другой, не перекодируя рабочий поток.

Ранее представленный в этой главе конечный автомат использует следующий интерфейс:

[ExternalDataExchange]
public interface IDoorService
{
   void LockDoor();
   void UnlockDoor();
   event EventHandler<ExternalDataEventArgs> RequestEntry;
   event EventHandler<ExternalDataEventArgs> OpenDoor;
   event EventHandler<ExternalDataEventArgs> CloseDoor;
   event EventHandler<ExternalDataEventArgs> FireAlarm;
   void OnRequestEntry(Guid id);
   void OnOpenDoor(Guid id);
   void OnCloseDoor(Guid id);
   void OnFireAlarm();
}

Интерфейс состоит из методов, используемых рабочим потоком для вызова службы, и событий, инициируемых службой, используемой рабочим потоком. Применение атрибута ExternalDataExchange указывает на то, что исполняющая среда рабочего потока используется для коммуникаций между работающим потоком и реализацией службы.

Внутри конечного автомата присутствует ряд экземпляров CallExternalMethodActivity, используемых для вызова методов этого внешнего интерфейса. Один пример — когда дверь запирается и отпирается. Рабочий поток при этом должен вызвать метод UnlockDoor или LockDoor, и служба реагирует на это посылкой соответствующей команды дверному замку.

Когда служба нуждается во взаимодействии с рабочим потоком, то делает это через события; исполняющая среда рабочих потоков также включает службу по имени ExternalDataExchangeService, которая служит прокси для таких событий. Этот прокси используется при возбуждении события, но поскольку рабочий поток может и не быть загруженным в память на момент доставки события, это событие сначала маршрутизируется внешней службе обмена, которая проверяет, загружен ли рабочий поток, и если нет, то восстанавливает его из постоянного хранилища и затем передает событие в этот рабочий поток.

Код, применяемый для конструирования ExternalDataExchangeService и прокси для событий, определенных в службе, показан ниже:

WorkflowRuntime runtime = new WorkflowRuntime();
ExternalDataExchangeService edes = new ExternalDataExchangeService();
runtime.AddService(edes);
DoorService service = new DoorService();
edes.AddService(service);

Это конструирует экземпляр внешней службы обмена, добавляет его в исполняющую среду, затем создает экземпляр DoorService (реализующий IDoorService) и добавляет его во внешнюю службу обмена данными.

Метод ExternalDataExchangeService.Add конструирует прокси для каждого события, определенного пользовательской службой, так что сохраненный рабочий поток может быть загружен до того, как будет доставлено событие. Если вы не помещаете службу внутрь внешней службы обмена данными, то когда вы станете инициировать события, никто не будет их прослушивать, поэтому они не будут доставлены соответствующему рабочему потоку.

События используют класс ExternalDataEventArgs, поскольку он включает идентификатор экземпляра рабочего потока, которому должно быть доставлено событие. Если существуют и другие значения, которые нужно передать из внешнего события в рабочий поток, то вы должны унаследовать класс от ExternalDataEventArgs и добавить требуемые значения в виде свойств в этот класс.

Хостинг рабочих потоков

Код для хостинга WorkflowRuntime в процессе будет варьироваться в зависимости от самого приложения.

Для приложений Windows Forms или Windows Service (служб Windows), типичным является конструирование исполняющей среды при запуске приложения и сохранение ее в качестве свойства главного класса приложения.

В ответ на некоторый ввод в приложении (например, щелчок пользователя на кнопке пользовательского интерфейса) вы можете затем сконструировать экземпляр рабочего потока и запустить этот экземпляр локально. Рабочему потоку может понадобиться взаимодействие с пользователем, поэтому, например, вы можете определить внешнюю службу, которая запросит пользовательского подтверждения, прежде чем отправит запрос серверу заднего плана.

При хостинге рабочих потоков в ASP.NET обычно вы не будете предупреждать пользователя через окно сообщения, а вместо этого выполните переход на другую страницу сайта, которая сначала затребует подтверждения и затем предоставит соответствующую страницу подтверждения. При развертывании исполняющей среды внутри ASP.NET принято переопределять событие Application_Start и конструировать в нем экземпляр исполняющей среды рабочего потока, который будет доступен всем прочим частям сайта. Вы можете сохранить экземпляр исполняющей среды в статическом свойстве, но лучше сохранить его в состоянии приложения и предусмотреть метод для доступа, который будет извлекать исполняющую среду из состояния приложения, чтобы ее можно было использовать в любом месте приложения.

При любом сценарии — Windows Forms или ASP.NET — вы будете конструировать экземпляр исполняющей среды рабочего потока и добавлять службы так, как показано ниже.

WorkflowRuntime workflowRuntime = new WorkflowRuntime();
workflowRuntime.AddService(
   new SqlWorkflowPersistenceService(conn, true, new TimeSpan(1,0,0),
   new TimeSpan(0,10,0)));
// Здесь выполнить рабочий поток...

Для выполнения рабочего потока понадобится создать экземпляр этого рабочего потока, вызвав для этого метод CreateInstance исполняющей среды. У этого метода есть ряд перегрузок, которые можно применять для конструирования экземпляра рабочего потока на основе кода либо рабочего потока, определенного с помощью XML.

Вплоть до этого места настоящей главы вы рассматривали рабочие потоки как классы .NET — и в самом деле это одно представление рабочего потока. Однако вы можете определить рабочий поток, используя для этого XML, а исполняющая среда создаст его представление в памяти и затем выполнит его, когда вы вызовете метод Start из WorkflowInstance.

Внутри Visual Studio вы можете создать рабочий поток на базе XML, выбрав элемент Sequential Workflow (with code separation) (Последовательный рабочий поток (с разделением кода)) или State Machine Workflow (with code separation) (Рабочий поток в виде конечного автомата (с разделением кода)) в диалоговом окне Add New Item (Добавить новый элемент). Это создаст XML-файл с расширением .xoml и загрузит его в конструктор.

По мере добавления действий в конструкторе они будут отражаться в XML, и структура элементов определит отношения “родительский–дочерний” между действиями. Приведенный ниже XML демонстрирует простой последовательный рабочий поток, содержащий IfElseAcvtivity и два кодовых действия, по одному на каждую ветвь IfElseAcvtivity.

<SequentialWorkflowActivity x:Class="DoorsWorkflow.Workflow1" x:
   Name="Workflow1"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
   <IfElseActivity x:Name="ifElseActivity1">
      <IfElseBranchActivity x:Name="ifElseBranchActivity1">
         <IfElseBranchActivity.Condition>
            <CodeCondition Condition="Test" />
         </IfElseBranchActivity.Condition>
         <CodeActivity x:Name="codeActivity1" ExecuteCode="DoSomething" />
      </IfElseBranchActivity>
      <IfElseBranchActivity x:Name="ifElseBranchActivity2">
         <CodeActivity x:Name="codeActivity2" ExecuteCode="DoSomethingElse" />
      </IfElseBranchActivity>
   </IfElseActivity>
</SequentialWorkflowActivity>

Свойства, определенные в действиях, сохраняются в XML в виде атрибутов, а каждое действие сохраняется как элемент. В XML несложно заметить, что отношение между родительскими действиями (такими как SequentialWorkflowActivity и IfElseActivity) и дочерними действиями определяется структурой.

Выполнение рабочего потока на основе XML никак не отличается от выполнения рабочего потока, описанного в коде — вы просто используете перегрузку метода CreateWorkflow, принимающего экземпляр XmlReader, и затем запускаете этот экземпляр, вызывая метод Start.

Одно преимущество рабочих потоков на основе XML перед рабочими потоками на основе кода заключается в том, что вы легко можете сохранить его определение в базе данных. Вы можете загрузить этот XML во время выполнения и запустить рабочий поток; кроме того, очень легко вносить изменения в определение рабочего потока без необходимости перекомпиляции какого-либо кода.

Изменение рабочего потока во время выполнения поддерживается независимо от того, определен ли он в XML или в коде. Вы просто конструируете объект WorkflowChanges, содержащий все новые действия, которые нужно добавить к рабочему потоку, и затем вызываете метод ApplyWorkflowChanges, определенный в классе WorkflowInstance, для фиксации этих изменений. Это исключительно удобно, поскольку бизнес-правила часто меняются и, к примеру, вам может понадобиться применить изменения к рабочему потоку политики страхования, чтобы клиенту отправлялось электронное письмо с соответствующим уведомлением за месяц до наступления даты изменений. Изменения вносятся на уровне экземпляра, поэтому если у вас есть 100 таких рабочих потоков политики в системе, вам придется внести изменения в каждый из них.

Конструктор Workflow Designer

И в завершение этой главы рассмотрим самое интересное. Конструктор Workflow Designer, который вы используете для проектирования рабочих потоков, не привязан к Visual Studio. При необходимости вы можете развернуть его в среде своего собственного приложения.

Это означает, что вы можете поставлять систему, включающую рабочие потоки, и позволить пользователям настраивать ее под свои нужды, без необходимости приобретения копии Visual Studio. Развертывание конструктора, однако, довольно-таки сложная задача, и ей можно было бы посвятить несколько глав. В Internet можно найти немало примеров развертывания этого конструктора, кроме того, рекомендуется почитать статью MSDN на данную тему, которая доступна по адресу http://msdn2.microsoft.com/en-us/library/aa480213.aspx.

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

В Windows Workflow это расширение становится в значительной степени графическим, поскольку вы можете предоставить пользователю пустой рабочий поток в качестве шаблона и обеспечить панелью инструментов, включающей специальные действия, которые отвечают нуждам вашего приложения. Затем пользователи самостоятельно смогут конструировать свои рабочие потоки, добавляя ваши действия или разрабатывая собственные.

Резюме

Windows Workflow приведет к радикальным изменениям в способе конструирования приложений. Вы можете теперь сформировать сложные части приложения в виде действий и позволить пользователям изменять работу системы простым перетаскиванием действий в рабочий поток.

Почти не существует приложений, к которым нельзя было бы применить концепцию рабочего потока — от простейших утилит командной строки до наиболее сложных систем, состоящих из многих сотен модулей. В то время как новые средства WCF и средства пользовательского интерфейса WPF являются значительным шагом вперед для приложений в целом, добавление Windows Workflow приведет к фундаментальным изменениям в способах разработки и конфигурирования приложений.

Если у вас есть время, чтобы потратить его на одно из новых средств .NET Framework 3.0, настоятельно рекомендуем обратиться именно к Windows Workflow. Есть мнения, что за несколько лет потребность в специалистах по рабочим потокам значительно вырастет.

Указания по загрузке

Чтобы загрузить каркас .NET Framework 3.0, обратитесь по адресу www.microsoft.com/downloads/details.aspx?FamilyId=10CC340B-F857-4A14-83F5-25634C3BF043.

Пакет Visual Studio 2005 Extensions for Windows Workflow Foundation доступен для отдельной загрузки по адресу www.microsoft.com/downloads/details.%20aspx?FamilyId=5D61409E-1FA3-48CF-8023-E8F38E709BA6.

В дополнение можно установить инструментальный набор .NET 3.0 SDK, поскольку там содержится множество примеров использования WF. Его вы найдете по адресу www.microsoft.com/downloads/details.aspx?familyid=C2B1E300-F358-4523B479-F53D234CDCCF.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 0    Оценка 46 [+0/-1]         Оценить