суббота, 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 был успешно сохранен во внешнем хранилище, а при повторном запуске приложения и попытке отправить ему события извлечен из оного. Что собственно говоря и требовалось по условию задачи.

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