Обзор рабочего потока Windows Workflow. Часть 2

Источник: rsdn

Читать часть 1

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

До сих пор вы имели дело с действиями, определенными внутри пространства имен 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 (прикрепленные свойства).

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

Читать часть 3


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=23675