Показаны сообщения с ярлыком Workflow. Показать все сообщения
Показаны сообщения с ярлыком Workflow. Показать все сообщения

суббота, 3 октября 2009 г.

Сервис постоянства в Workflow Foundation

Для одного маленького, но очень интересного проекта решили применить workflow. И сразу необходимо было решить задачу сохранения работающих workflow во внешних хранилищах между запусками клиентских приложений.
В русскоязычном интернете этой проблеме уделено весьма мало внимания... Где можно почитать? Я нашел вот эти работы: на rsdn, gotdotnet и т.д..
Эти статьи либо достаточно поверхностны, либо подразумевают (как последняя) что вы уже все знаете :).
Поэтому давайте попробуем решить поставленную задачу (сохранение workflow во внешнем хранилище) медленно и по шагам.
Итак, для решения данной задачи нам понадобится VS 2008 + MS SQL (версия последнего не особенно принципиальна).
1. Идем сюда и читаем как нам подготовить MS SQL.
2. Создаем простенький workflow на котором будем ставить эксперименты. Для простоты я воспользовался следующим:

Первое состояние стартовое и мы его сразу покидаем. Второе вызывает метод Print из нижеописанного интерфейса и ждет событие из него же. Третье служит для того чтобы сказать что событие успешно получено и обработано.
Обещанный интерфейс:
  [ExternalDataExchangeAttribute()]
  public interface IMethodAndEvents
  {
    event EventHandler<ExternalDataEventArgs> MyEvent;

    void PrintText(string p_text);
  }


* This source code was highlighted with Source Code Highlighter.

2. Тестировать будем при помощи формы следующего вида:

Кнопки 2 и 3 создают WorkflowRuntime и все необходимые сервисы, а кроме того кнопка 2 создает экземпляр workflow.
    private void button2_Click(object sender, RoutedEventArgs e)
    {
      button3_Click(null, null);

      _instance = _runtime.CreateWorkflow(typeof(Workflow1));
      _instance.Start();
      textBox1.Text = _instance.InstanceId.ToString();
    }

    private void button3_Click(object sender, RoutedEventArgs e)
    {
      _runtime = new WorkflowRuntime();

      // Create the SqlWorkflowPersistenceService.
      string connectionString = "Initial Catalog=WorkflowPersistenceStore;Data Source=localhost;Integrated Security=SSPI;";
      bool unloadOnIdle = true;
      TimeSpan instanceOwnershipDuration = TimeSpan.MaxValue;
      TimeSpan loadingInterval = new TimeSpan(0, 2, 0);
      SqlWorkflowPersistenceService persistService = new SqlWorkflowPersistenceService(connectionString, unloadOnIdle, instanceOwnershipDuration, loadingInterval);
      _runtime.AddService(persistService);


      ExternalDataExchangeService externalDataExchangeService = new ExternalDataExchangeService();
      _runtime.AddService(externalDataExchangeService);
      externalDataExchangeService.AddService(this);

      _runtime.StartRuntime();
    }


* This source code was highlighted with Source Code Highlighter.


Как видим из метода 2 кнопки Id workflow выводится в textBox.

Кнопка 1 отправляет событие в workflow идентификатор которого берет из поля формы:
    private void button1_Click(object sender, RoutedEventArgs e)
    {
      if (MyEvent != null)
      {
        MyEvent(null, new ExternalDataEventArgs(_instance.InstanceId));
      }
    }


* This source code was highlighted with Source Code Highlighter.

А вот кнопка 4 отправляет событие в workflow чей идентификатор берется из textBox-а.
    private void button4_Click(object sender, RoutedEventArgs e)
    {
      if (MyEvent != null)
      {
        Guid id = new Guid(textBox1.Text);
        MyEvent(null, new ExternalDataEventArgs(id));
      }
    }


* This source code was highlighted with Source Code Highlighter.


В чем отличие? Если в textBox попадает тот же идентификатор что хранится в поле _instance? смотрим следующий шаг!
3. Запускаем приложение и тестируем работу workflow кликнув на кнопке 2, а затем 1. Поток отрабатывает как и ожидалось, в чем мы можем убедится за счет вызова метода Print например такого содержания:
    delegate void PrintHandler(string p_text);

    public void PrintText(string p_text)
    {
      // Проверяем совпадает ли поток диспетчера с потоком вызвавшим метод
      if (Dispatcher.Thread == Thread.CurrentThread)
      {
        // Все замечательно :) меняем значение textbox-а
        label1.Content = p_text;
      }
      else
      {
        // Нет :( все плохо :( перезапускаем метод в потоке диспетчера
        Dispatcher.Invoke(new PrintHandler(PrintText), new object[] { p_text });
      }    
    }


* This source code was highlighted with Source Code Highlighter.


А теперь самое интересное! Перезапускаем приложение нажимаем на кнопку 2, копируем в буфер обмена значение идентификатора и?..

Перезапускаем приложение!
В новой копии приложения создаем рунтайм (но не workflow) кнопкой 3. Вставляем из буфера в textBox идентификатор. Нажимаем на кнопку 4 и видем в label подтверждение обработки события в workflow.

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

На этом можно бы и закончить, но рекомендую попробовать написать все это самостоятельно ручками ;)

вторник, 26 мая 2009 г.

Взаимодействие WF с программным окружением

Продолжим разработку нашего примера с workflow. В данной части мы посмотрим как из WF взаимодествовать с программным окружением. В качестве конечного автомата возьмем конечный автомат от сюда.
В данном примере, для наглядности, давайте создадим приложение с GUI:

Кнопки соответствуют событиям, прямоугольники показывают состояние двери и замка (красный - закрыто, зеленый открыто).
Для взаимодействия с программным окружением используются следующие Activity:
1. СallExternalMethodActivity – используется для вызова метода внешнего по отношению к WF (основные свойства InterfaceType и MethodName)

2. EventDrivenActivity – используется для перехвата событий из кода внешнего по отношению к WF
3. HandleExternalEventActivity – помещается в EventDrivenActivity и ожидает события (основные свойства InterfaceType и EventName)


К событиям передаваемым из внешнего окружения в WF предъявляются следующие требования:
1. Событие должно быть потомок EventHandler
2. В качестве T должен быть любой класс потомок ExternalDataEventArgs
Кстати, конструктор ExternalDataEventArgs принимает Guid потока которому предназначается сообщение.

Для взаимодействия с WF объекты должны являться, во-первых, наследниками интерфейса помеченного атрибутом [ExternalDataExchange].
Введем два интерфейса:
  [ExternalDataExchange]
  public interface IDoor
  {
    void OpenLock();
    void OpenDoor();
    void CloseDoor();
    string GetKey();
  }
  [ExternalDataExchange]
  public interface IVisitor
  {
    event EventHandler<ExternalDataEventArgs> TestKey;
    event EventHandler<ExternalDataEventArgs> Open;
    event EventHandler<ExternalDataEventArgs> Close;
  }

* This source code was highlighted with Source Code Highlighter.

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

Кстати, если методо вызываемый из WF возвращает значение, то его можно непосредственно в дизайнере привязать к полю WF, использовав для этгого свойство ReturnValue:


Добавив везде где это надо вместо CodeActivity вызов внешних методов и перехват внешних событий, получим конечный автомат вида:


Реализация интерфейса IDoor может иметь вид:
    #region IDoor Members
    public void OpenLock()
    {
      if (Dispatcher.Thread != Thread.CurrentThread)
      {
        Dispatcher.Invoke(new NoParamHandler(OpenLock), new object[] { });
      }
      else
      {
        rcDoorlock.Fill = new SolidColorBrush(Color.FromRgb(0, 255, 0));
      }
    }
    public void OpenDoor()
    {
      if (Dispatcher.Thread != Thread.CurrentThread)
      {
        Dispatcher.Invoke(new NoParamHandler(OpenDoor), new object[] { });
      }
      else
      {
        rcDoor.Fill = new SolidColorBrush(Color.FromRgb(0, 255, 0));
      }
    }
    private delegate void NoParamHandler();
    public void CloseDoor()
    {
      if (Dispatcher.Thread != Thread.CurrentThread)
      {
        Dispatcher.Invoke(new NoParamHandler(CloseDoor), new object[] { });
      }
      else
      {
        rcDoorlock.Fill = new SolidColorBrush(Color.FromRgb(255, 0, 0));
        rcDoor.Fill = new SolidColorBrush(Color.FromRgb(255, 0, 0));
      }
    }
    public string GetKey()
    {
      if (Dispatcher.Thread == Thread.CurrentThread)
      {
        return tbKey.Text;
      }
      else
      {
        return (string)Dispatcher.Invoke(new Func<string>(GetKey), new object[] { });
      }
    }
    #endregion


* This source code was highlighted with Source Code Highlighter.


Ну и в завершении как же создать host для WF. На самом деле все достаточно просто:
  1.     WorkflowRuntime workflowRuntime;
  2.     WorkflowInstance instance;
  3.     StateMachineWorkflowInstance machine;
  4.  
  5.     private void Window_Loaded(object sender, RoutedEventArgs e)
  6.     {
  7.       workflowRuntime = new WorkflowRuntime();
  8.       workflowRuntime.WorkflowStarted += delegate(object s, WorkflowEventArgs e1) { MessageBox.Show("Рабочий поток начал работу"); };
  9.       workflowRuntime.WorkflowCompleted += delegate(object s, WorkflowCompletedEventArgs e1) { MessageBox.Show("Рабочий поток закончил работу"); };
  10.       workflowRuntime.WorkflowTerminated += delegate(object s, WorkflowTerminatedEventArgs e1)
  11.       {
  12.         MessageBox.Show(e1.Exception.Message);
  13.       };
  14.       ExternalDataExchangeService externalDataExchangeService = new ExternalDataExchangeService();
  15.       workflowRuntime.AddService(externalDataExchangeService);
  16.       externalDataExchangeService.AddService(this);
  17.       
  18.       instance = workflowRuntime.CreateWorkflow(typeof(Workflow1));
  19.       machine = new StateMachineWorkflowInstance(workflowRuntime, instance.InstanceId);
  20.       instance.Start();
  21.  
  22.     }
* This source code was highlighted with Source Code Highlighter.


В строках 7-13 мы создаем среду выполнения WF и подписываемся на основные ее обработчики.
В строках 14-16 создаем сервис для взаимодействия с WF, регистрируем его в среде выполнения, и добавляем объект методы которого необходимо вызывать и события обрабатывать (в данном случае это текущая форма).
В строках 18,20 создается WF и запускается на выполнение.
В строке 19 получаем ссылку на WF в виде StateMachine которую можно использовать для проверки текущего состояния конечного автоамта, принудительного перевода из состояния в состояние и т.д.

среда, 20 мая 2009 г.

Пример создания простого конечного автомата

Для создания конечного автомата можно воспользоваться уже существующим шаблоном Visual Studio - "Консольное приложение рабочих процессов конечного автомата".
В результате будет создано консольное приложение включающее конечный автомат и класс содержащий метод Main в который уже добавлен весь код необходимый для запуска конечного автомата.
Основным элементом для построения конечных автоматов является компонент: StateActivity, который и задает состояния конечного автомата.
В качестве примера давайте рассмотри пример конечного автомата, эмитирующего работу двери с кодовым замком:
Состояниями указанного автомата будут являться:
1. Начальное состояние
2. Дверь закрыта, замок защелкнут
3. Замок проверяет код
4. Дверь закрыта, замок открыт
5. Дверь открыта
События, которые должен принимать автомат, будут следующие:
1. Набор кода
2. Код введен правильно
3. Код введен не правильно
4. Открывание двери
5. Закрывание двери
Схематически данный автомат можно изобразить следующим образом:


Добавив 4 состояния в конечный автомата (начальное состояние добавлено автоматически), получим следующий автомат:

Обычно состояние конечного автомата состоит из трех программных компонентов:
1. StateInitializationActivity – выполняет операции при переходе автомата в состояние.
2. Один или несколько EventDrivenActivity – отвечают за перехват сообщений из внешнего мира
3. StateFinalizationActivity – выполняет операции перед тем как автомат покинет текущее состояние.
Ни один из указанных компонентов не является обязательным.
Для начала поместим в Workflow1InitialState два компонента: StateInitializationActivity и StateFinalizationActivity (с компонентом EventDrivenActivity мы познакомимся в следующий раз).
В результате получим:

Двойной клик на любой Activity добавленной в состояние открывает линейный workflow в котором задаются действия выполняемые при передачи управления данному Activity.

Добавим компонент CodeActivity, который позволяет выполнить закрепленный за ним обработчик. Добавим в него код вида:
private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Дверь создана и готова к работе");
}


* This source code was highlighted with Source Code Highlighter.


Для перехода в другое состояние используется SetStateActivity, состояние в которое необходимо перейти в данном Activity задается свойством: TargetStateName. В нашем случае его необходимо задать в CloseClose:


Добавим в состояние CloseClose StateInitializationActivity аналогичный Workflow1InitialState указав в качестве выполняемого кода следующий метод (в класс Workflow1 также добавляем строковое поле класса string):
string code = "";

private void codeActivity2_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Введите код:");
code = Console.ReadLine();
}


* This source code was highlighted with Source Code Highlighter.
У SetStateAtivity укажем в качестве TargetStateName состояние TestKey.
В состоянии TestKey в StateInitializationActivity помести проверку ключа на равенство «111». Для этого воспользуемся компонентом IfElseActivity. Все ветви кроме одной данного Activity должны иметь проинициализированным свойство Conditon. В неашем примере установим значение этого свойства в «Declarative Rule Condition» (т.е. само Activity проверяет некое декларативное правило задаваемое при помощи мастера доступного в свойстве ConditionName. Для провекри на соответствие введенного кода введм следующее правило: this.code == "111".
После чего в левую ветвь поместим SetStateActivity с переходом на CloseOpen, а в правую с переходом на CloseClose.

Я думаю, остальные состояния трудности при написании не вызовут. Попробуйте написать их самостоятельно. В результате должно получится что то похожее:

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