среда, 25 ноября 2009 г.

Тестовые данные в SketchFlow

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

понедельник, 23 ноября 2009 г.

Создание окна из потока отличного от потока диспетчера приложения

Возникла задача, в процессе вызова метода из workflow показать форму для ввода дополнительных данных (для тех, кто не в курсе - workflow выполняются в отдельном потоке, и соответственно если метод вызывается из workflow, он тоже выполняется в этом потоке). Приложение написано на WPF. Привычная последовательность действий:
    public void StartReadFromScala(Guid idWorkflow)
    {
      WndReadFromScala form = new WndReadFromScala();
      form.Show();
    }


* This source code was highlighted with Source Code Highlighter.

Выполнялось, но форма не появлялась :(

Пришлось пойти на следующее ухищрение:
    public void StartReadFromScala(Guid idWorkflow)
    {
      // Создаем форму для ввода данных, но создаем ее в главном потоке
      Application.Current.Dispatcher.Invoke(new NoParamHandler(CreateWndReadFromScala), new object[] { });
    }

    private void CreateWndReadFromScala()
    {
      WndReadFromScala form = new WndReadFromScala();
      form.Show();
    }


* This source code was highlighted with Source Code Highlighter.

А вот теперь все работает ;)

вторник, 3 ноября 2009 г.

Олимпиада по многократному решению делемы заключенного

Было принято решение провести олимпиаду по решению многократной дилемы заключенного.
К участию приглашаются все желающие. Дата проведения олимпиады будет объявлена дополнительно. Ориентировочно олимпиада будет проходить 17 декабря 2009 года в 16:00.

Для участия необходимо предоставить dll разработанную на платформе .Net и содержащую класс потомок интерфейса:
namespace Competition
{
  public interface ICompetition
  {
    /// <summary>
    /// Передает в библиотеку количество участников
    /// </summary>
    /// <param name="p_count">количество участников</param>
    void SetCount(int p_count);
    /// <summary>
    /// Делает один ход
    /// </summary>
    /// <param name="p_prevResult">Массив предыдущих ответов соответствующих участников</param>
    /// <returns>Массив ответов на текущем шаге</returns>
    bool[] MakeStep(bool[] p_prevResult);
  }
}


* This source code was highlighted with Source Code Highlighter.

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

В процессе проведения соревнования запрещается:
1. Попытка загрузки, рефакторинга, изменения или удаления сборок оппонентов.
2. Запрещается влиять на работу сервера иными средствами кроме возвращения массива из метода MakeStep.
3. Осуществлять сговор участников соревнования.
4. Модифицировать ICompetition.dll или реализовывать интерфейс ICompetition в своих сборках.


Порядок проведения соревнования:
1. Все участники соревнования предоставляют dll удовлетворяющие описанным выше требованиям и запретам.
2. Сервер олимпиады подгружает все зарегистрированные сборки с загрузкой из каждой ОДНОГО класса потомка интерфейса ICompetition.
3. От каждого класса создается объект посредством вызова конструктора по умолчанию.
4. Для каждого созданного объекта вызывается метод SetCount в который передается количество участников соревнований.
5а. В первом туре в метод MakeStep передается null. Метод должен вернуть в виде массива bool-евых значений свое отношение к сборкам противников: true - сотрудничать, false - предавать. Количество значений в массиве должно соответствовать количеству участников полученных классом через метод SetCount.
5б. Во втором и последующих турах (проходит от 30 до 50 туров), в метод MakeStep передается отношение к данному участнику всех оппонентов. Количество значений в массиве соответствует количеству участников полученных классом через метод SetCount. На основе полученных данных необходимо вернуть массив отношения к сборкам противников.
6. Через 5 секунд после вызова соответствующих методов, все методы не закончившие обработку и не вернувшие значение снимаются с соревнований.
7. Если количество туров превысило 30 и сгенерированное случайное число меньше 1/20 то соревнование заканчивается, в противном случае переход в пункт 5б.

Если два участника поставили взаимное true, то каждый из них получает по 7 очков рейтинга. Если два участника поставили взаимное false, то каждый из них получает по 3 очка рейтинга. Если один из участников поставил true в ответ на false другого участника, то первый не получает очки рейтинга, а второй получает 10 очков рейтинга.

Пять программ набравших наибольшее количество баллов рейтинга получают по 5,4,3,2 и 1 очку в зависимости от занятого места.
Соревнования повторяются 3-5 раз. Программа набравшая суммарно максимальное количество очков признается победителем олимпиады.

В качестве приза будет нечто полезное программистам ;)

Удачной олимпиады.

P.s. В случае обнаружения неточностей, ошибок или при наличии предложений писать в комментариях.

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

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

среда, 12 августа 2009 г.

Выполнение кода после изменения текста в ComboBox

Стоит задача после изменения значения в ComboBox, вывести некоторое сообщение пользователю, например, в MessageBox.
В XAML все просто:
<ComboBox SelectionChanged="ComboBox_SelectionChanged" >
  <ItemsControl>1</ItemsControl>
  <ItemsControl>2</ItemsControl>
  <ItemsControl>3</ItemsControl>
  <ItemsControl>4</ItemsControl>
</ComboBox>

* This source code was highlighted with Source Code Highlighter.

Но если в обработчике события тупо вызывать MessageBox.Show возникает проблема: пользователь уже видит MessageBox, а изменение текста еще не произошло :( Что может ввести пользователя в заблуждение.
Решить проблему можно вот таким достаточно извращенным способом:
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  DispatcherTimer t = null;
  t = new DispatcherTimer(new TimeSpan(1), DispatcherPriority.Background, (s, e1) => { t.Stop(); MessageBox.Show("asdfvasdvc"); }, this.Dispatcher);
  t.Start();
}

* This source code was highlighted with Source Code Highlighter.


Но надо будет еще подумать... что то ведь должно быть проще.

понедельник, 13 июля 2009 г.

Получение размеров экрана и рабочей области в WPF приложениях

Возникла задача разместить окно приложения в правом нижнем углу.
Можно решить задачу так:

this.Left = SystemParameters.WorkArea.Width - this.Width;
this.Top = SystemParameters.WorkArea.Height - this.Height;

Если применить:

this.Top = SystemParameters.VirtualScreenHeight - this.Height;

то часть окна будет скрыта панелью задач.

вторник, 23 июня 2009 г.

Путь к каталогу от куда запущено приложение в WPF

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

Патерн "Фабрика объектов" на примере "Фабрики форм"

При работе над проектом "Электронный университет" давно используем "Фабрику форм", но там задачи спецефические, да и времени небыло, поэтому документация в достаточно разрозненном виде, да и отягчена работой с AzMan-ом, Web-сервисами и прочими вещами. Поэтому в данной статье попробую расказать основные идеи данного патерна. Итак, начнем.
Задача:
Необходимо реализвать приложение загружающее формы в соответствии с правами пользователя. Или, говоря простым языком, каждый пользователь после аутентификации должен получать главное меню ориентированное под его задачи. Ингода формы связанные с пунктами меню должны быть сразу загружены.
Средства достижения:
1. Reflection - используется для динамического создания объектов
2. Интерфейc IFactoryForm - для того чтобы мы спросили у созданной формы, нужен ли ей пункт меню, а также получили возможность передавать форме команды из главного приложения, а также получать команды из формы, для передачи в другие формы (во завернул) .
3. Enum содержащий все команды - для организации взаимодействия форм между собой в процессе работы.
4. Интерфейс IParam для передачи параметров команды. Кстати этот интерфейс ничего не содержит и создан только для того, чтобы при передаче параметорв команды разработчик работающий с фабрикой понимал, что он передает не просто объект, а объект параметр команды.
5. Ну и собственно сама фабрика - часть кода выполняющая работу с загружаемымми формами.

Давайте начнем в таком порядке и обсуждать.
1. Reflection - написано очень много всего хорошего, поэтому здесь я останавливаться не буду. Кто с Reflection не работал может посмотреть в msdn.
2. Как я уже сказал интерфейс используется для взаимодействия фабрики с формой. Для ростоты использования реализовывать лучше в отдельной dll. В самом простом случае должен иметь вид:

Код интерфейса будет иметь вид:
  public interface IFactoryForm
  {
    event CommandHandler NewCommand;

    /// <summary>
    /// Возвращает с каким пунктом меню необходимо связать форму.
    /// </summary>
    /// <returns>
    /// Путь до пункта меню добавляется форма. Путь имеет вид:
    /// Item-&gt;Item-&gt;Item
    /// Если строка пустая, то пункт меню создавать не надо и форма из пункта меню не вызывается
    /// </returns>
    string GetMenuItem();

    /// <summary>
    /// Вызывается при клике пользователя на пункте меню связанном с формой
    /// </summary>
    void Activate(object sender, EventArgs e);

    /// <summary>
    /// Вызывается главной формой приложения для передачи команд в форму
    /// </summary>
    void DoCommand(Commands p_command, IParam p_param);
  }


* This source code was highlighted with Source Code Highlighter.


3. Перечисление Commands (см. предыдущий рисунок) будет содержать команды которыми обмениваются формы в процессе работы приложения, для начала это всего одна команда которая нам нужна для показа тестовой формы в нашем приложении.
4. Опять же, как видно из рисунка IParam ничего не содержит ;)
5. Ну а теперь перейдем к фабрике.
Фабрика будет реализованна в виде пары методов и вспомогательного события.
Итак первый метод отвечает за создание форм. В нашем проекте идентификация пользователей идет при помощи штрих-кодов с бейджиков. Поэтому главная форма приложения после запуска имеет вид:

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

Код метода:
/// <summary>
    /// Список форм созданных при помощи фабрики
    /// </summary>
    List<IFactoryForm> formsFromFactory;

    private void CreateForms()
    {
      // Показываем форму идентификации
      FrmLogin login = new FrmLogin();
      login.StartPosition = FormStartPosition.CenterParent;
      if (login.ShowDialog() == DialogResult.OK)
      {
        // если штрих-корд считан, то получаем перечень форм для загрузки.
        // Реализация метода может быть любая. Мы, например, берем из базы данных
        FactoryFormData.FormsDataTable forms = GetFormsForPerson(new Guid(login.tbPersonId.Text));
        // Строка для сбора сообщений об ошибках работы
        StringBuilder errors = new StringBuilder();
        // Пробегаем по всем формам и загружаем их в приложение
        foreach (FactoryFormData.FormsRow item in forms)
        {
          // Получаем и загружаем сборку
          Assembly dll = null;
          try
          {
            dll = Assembly.LoadFile(Application.StartupPath + '\\' + item.Assembly);
          }
          catch{}
          if (dll != null)
          {
            // Сборка загружена. загружаем форму
            Type currentFormClass = dll.GetType(item.FormFullName);
            if (currentFormClass != null)
            {
              // Создаем объект и приводим его к IFactoryForm
              ConstructorInfo ci = currentFormClass.GetConstructor(new Type[] { });
              IFactoryForm currentForm = ci.Invoke(new object[] { }) as IFactoryForm;
              formsFromFactory.Add(currentForm);
              ((Form)currentForm).MdiParent = this;
              if (currentForm != null)
              {
                // Собственно форма в памяти, осталось только создать меню и привязать форму к нему
                string menuItemText = currentForm.GetMenuItem();
                switch (menuItemText)
                {
                  case "":
                    // Собственно делать ничего не нужно
                    break;
                  case "_show_":
                    // Форме пункт меню не нужен, форма требует немедленного показа
                    ((Form)currentForm).Show();
                    break;
                  default:
                    // Создаем пункт меню
                    ToolStripMenuItem mi = null;
                    string[] path = menuItemText.Split(new string[] { "->" }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string currentMenuItemText in path)
                    {
                      ToolStripItemCollection itemsForCheck = null;
                      if (mi == null)
                      {
                        // если мы на вершине иерархии, то ищем пункт меню в главном меню
                        itemsForCheck = msMain.Items;
                      }
                      else
                      {
                        // а если нет, то в подпунктах текущего пункта
                        itemsForCheck = mi.DropDownItems;
                      }
                      ToolStripMenuItem childItem = itemsForCheck.Cast<ToolStripMenuItem>().FirstOrDefault(x => x.Text == currentMenuItemText);
                      if (childItem == null)
                      {
                        // элемент с таким имененм не найден, содаем
                        childItem = new ToolStripMenuItem(currentMenuItemText);
                        if (mi == null)
                        {
                          msMain.Items.Insert(1, childItem);
                        }
                        else
                        {
                          mi.DropDownItems.Add(childItem);
                        }
                        mi = childItem;
                      }
                      mi = childItem;
                    }
                    // Итак сейчас в mi должна быть ссылка на пункт меню к которому привязана форма
                    // Подписываем форму на клик по этому пункту меню
                    mi.Click += currentForm.Activate;
                    break;
                }
              }
              else
              {
                errors.Append(string.Format("Ошибка загрузки формы: {0}. Форма не поддерживает IFactoryForm.\n", item.FormFullName));
              }
            }
            else
            {
              errors.Append(string.Format("Ошибка загрузки Формы: {0}. Форма не найдена в сборке.\n", item.FormFullName));
            }
          }
          else
          {
            errors.Append(string.Format("Ошибка загрузки бибилотеки: {0}. Бибилотека не найдена.\n", item.Assembly));
          }
          if (errors.Length != 0)
          {
            MessageBox.Show(errors.ToString());
          }
        }
      }
    }


* This source code was highlighted with Source Code Highlighter.


Реализация интерфейса у тестовой формы (конечно размещеной в другой сборке, иначе фабрика теряет смысл):
  public partial class TestForm : Form, IFactoryForm
  {
    public TestForm()
    {
      InitializeComponent();
    }

    #region Члены IFactoryForm

    public event CommandHandler NewCommand;

    public string GetMenuItem()
    {
      return "Тестовые формы->Первая тестовая";
    }

    public void Activate(object sender, EventArgs e)
    {
      DoCommand(Commands.ShowFirstForm, null);
    }

    public void DoCommand(Commands p_command, IParam p_param)
    {
      switch (p_command)
      {
        case Commands.ShowFirstForm:
          this.Show();
          break;
      }
    }

    #endregion
  }


* This source code was highlighted with Source Code Highlighter.


Вуаля, все заработало!
Теперь задание на дом, придумать как при помощи имеющегося события NewCommand, метода DoCommand и списка List formsFromFactory реализовать взаимодействие порожденных фабрикой форм между собой, если известно что ссылок друг на друга они не имеют.

четверг, 4 июня 2009 г.

Мысль в слух

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

cbItemOfExpenses.ItemsSource = _budget.ItemOfExpenses.Where(dr => dr.IsReceipts == _isReceipts).OrderBy(dr => dr.Name);

Или вот, формирование агрегатного запроса:
var items = from item in first
group item by item.IdItemOfExpenses into gr              
select
new
{
  Name = _budget.ItemOfExpenses.First(dr => dr.IdItemOfExpenses == gr.Key).Name,                
  Receipts = gr.Sum(dr => dr.IsReceipts ? (double?)dr.Money : 0d),
  Expense = gr.Sum(dr => dr.IsReceipts ? 0d : (double?)dr.Money)
};


* This source code was highlighted with Source Code Highlighter.

Ну разве не замечательно?

четверг, 28 мая 2009 г.

Шаблоны в WPF

В основе идеи шаблонов лежит концепция отделения функциональности от внешнего вида.
Посмотрите на вот этот рисунок:

Как видно на кнопке размещается CheckBox, картинка, набор RadioButton и даже еще одна кнопка! И все это, как не странно, работает. Достигается это за счет того, что основная масса компонентов имеет свойство Content, в рамках которого мы можем создавать практически любое наполнение. Основной недостаток, мы не можем переопределить саму базовую кнопку. Например, сделать ее треугольной или круглой, хотя цвет, размер и много другое изменить достаточно легко.
Для изменения всего способа отображения и используются шаблоны.
В WPF существует несколько видов шаблонов:
1. ControlTemplate - позволяет задавать шаблон для любого визуального компонента.
2. ItemsPanelTemplate - позволяет задавать шаблон компоновки для контейнеров.
3. DataTemplate - задает шаблоны отображения данных.
4. HierarchicalDataTemplate - задает шаблоны древовидных структур.
Рассмотрим самый простой случай ControlTemplate:
  1.   <Window.Resources>
  2.     <Style TargetType="{x:Type Button}">
  3.       <Setter Property="Background" Value="Black" />
  4.       <Setter Property="Height" Value="40" />
  5.       <Setter Property="Foreground" Value="White" />
  6.       <Setter Property="Margin" Value="3" />
  7.       <Setter Property="Template">
  8.         <Setter.Value>
  9.           <ControlTemplate TargetType="{x:Type Button}">
  10.             <Grid>
  11.               <Rectangle Name="GelBackground" RadiusX="9" RadiusY="9" Fill="Black">                
  12.               </Rectangle>
  13.               <Rectangle Name="GelShine" Margin="2,2,2,0" VerticalAlignment="Top" RadiusX="6" RadiusY="6" Height="15px">
  14.                 <Rectangle.Fill>
  15.                   <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
  16.                     <GradientStop Offset="0" Color="#ccffffff" />
  17.                     <GradientStop Offset="1" Color="Transparent" />
  18.                   </LinearGradientBrush>
  19.                 </Rectangle.Fill>
  20.               </Rectangle>
  21.               <ContentPresenter Name="GelButtonContent" VerticalAlignment="Center" HorizontalAlignment="Center" Content="{TemplateBinding Content}" />
  22.             </Grid>
  23.             <ControlTemplate.Triggers>
  24.               <Trigger Property="IsMouseOver" Value="True">
  25.                 <Setter Property="Rectangle.Fill" TargetName="GelBackground">
  26.                   <Setter.Value>
  27.                     <RadialGradientBrush>
  28.                       <GradientStop Offset="0" Color="Lime" />
  29.                       <GradientStop Offset="1" Color="DarkGreen" />
  30.                     </RadialGradientBrush>
  31.                   </Setter.Value>
  32.                 </Setter>
  33.                 <Setter Property="Foreground" Value="Black" />
  34.               </Trigger>
  35.               <Trigger Property="IsPressed" Value="True">
  36.                 <Setter Property="Rectangle.Fill" TargetName="GelBackground">
  37.                   <Setter.Value>
  38.                     <RadialGradientBrush>
  39.                       <GradientStop Offset="0" Color="#ffcc00" />
  40.                       <GradientStop Offset="1" Color="#cc9900" />
  41.                     </RadialGradientBrush>
  42.                   </Setter.Value>
  43.                 </Setter>
  44.               </Trigger>
  45.             </ControlTemplate.Triggers>
  46.           </ControlTemplate>
  47.         </Setter.Value>
  48.       </Setter>
  49.     </Style>
  50.   </Window.Resources>
  51.   <Grid>
  52.     <Button Width="124">Кнопка с шаблоном</Button>
  53.   </Grid>
* This source code was highlighted with Source Code Highlighter.


Как видно мы задаем новый стиль (строка 2), который переопределяет свойство Template - как раз тот самый шаблон (строка 7 и далее). Все остальное достаточно понятно, кроме наверно строки 17 где цвет задается не как конкретный цвет (напрмер - Red), а как переход к цвету подложки - Transparent. И конечно строка 21 в которой мы задаем, что в нашем шаблоне кнопки в качестве контента должно быть то, что помещено в контент кнопки к которой применен шаблон. В строках 24 и 25 задаются тригеры для изменения цвета кнопки при наведении мышки и при нажатии.
Вот так все это выглядит в обычном режиме, при наведении мышки и нажатии.

Кстати, попробуйте сделать круглую кнопку ;)

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

четверг, 21 мая 2009 г.

Статьи про работу с TaskBar в Windows 7

Программируем Windows 7: Taskbar. Часть 1 — Progress Bar
Программируем Windows 7: Taskbar. Часть 2 — ThumbButtons
Программируем Windows 7: Taskbar. Часть 3 – OverlayIcon
Программируем Windows 7: Taskbar. Часть 4 – Custom OverlayIcon
Программируем Windows 7: Taskbar. Часть 5 – CustomWindowsManager
Программируем Windows 7: Taskbar. Часть 6 – AppId
Программируем Windows 7: Taskbar. Часть 7 – ThumbnailClip
Программируем Windows 7: Taskbar. Часть 9 – PeekBitmap
Программируем Windows 7: Taskbar. Часть 10 (заключительная) – JumpLists

Триггеры в WPF

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

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

  1. <Style TargetType="{x:Type TextBox}">
  2.       <Setter Property="TextBox.Background">
  3.         <Setter.Value>
  4.           <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
  5.             <GradientStop Offset="0.0" Color="LightCyan" />
  6.             <GradientStop Offset="0.14" Color="Cyan" />
  7.             <GradientStop Offset="0.7" Color="DarkCyan" />
  8.           </LinearGradientBrush>
  9.         </Setter.Value>
  10.       </Setter>
  11.       <Style.Triggers>
  12.         <Trigger Property="IsFocused" Value="True">
  13.           <Setter Property="TextBox.Background">
  14.             <Setter.Value>
  15.               <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
  16.                 <GradientStop Offset="0.0" Color="LightSeaGreen" />
  17.                 <GradientStop Offset="0.14" Color="SeaGreen" />
  18.                 <GradientStop Offset="0.7" Color="DarkSeaGreen" />
  19.               </LinearGradientBrush>
  20.             </Setter.Value>
  21.           </Setter>
  22.         </Trigger>
  23.       </Style.Triggers>
  24.     </Style>
* This source code was highlighted with Source Code Highlighter.


Итак, в первой строке задается картинка, для которой во второй строке мы открываем задание набора тригеров. Тригеры у нас заданы в строках 3 и 11. Превый привязан к событию захода мышки в компонент, второй к покиданию мышкой компонента.
В связи с тем, что свойство Width компонента Image имеет тип double мы и применяем в строках 6 и 14 DoubleAnimation. Задав целевое свойство (Storyboard.TargetProperty), продолжительность анимации (Duration) и к какому значению надо привести (To). В результете при навидении мышки на картинку она будет увеличиваться в размере.
Т.к. для каждой картинки в приложении особенно этого не наделаешься, то само собой можно применить стиль...
Например, вот такой:

  1. <Style TargetType="{x:Type Image}">
  2.       <Setter Property="Image.Width" Value="100"></Setter>
  3.       <Style.Triggers>
  4.         <EventTrigger RoutedEvent="Image.MouseEnter">
  5.           <BeginStoryboard>
  6.             <Storyboard >
  7.               <DoubleAnimation Storyboard.TargetProperty="Width" BeginTime="00:00:00" Duration="00:00:01"   From="100" To="200">
  8.               </DoubleAnimation>
  9.             </Storyboard>
  10.           </BeginStoryboard>
  11.         </EventTrigger>
  12.         <EventTrigger RoutedEvent="Image.MouseLeave">
  13.           <BeginStoryboard>
  14.             <Storyboard >
  15.               <DoubleAnimation Storyboard.TargetProperty="Width" BeginTime="00:00:00" Duration="00:00:01"    To="100">
  16.               </DoubleAnimation>
  17.             </Storyboard>
  18.           </BeginStoryboard>
  19.         </EventTrigger>
  20.       </Style.Triggers>
  21.     </Style>
* This source code was highlighted with Source Code Highlighter.


Теперь все картинки не имеющие переопределений стиля по умолчании будут иметь ширину 100, при наведнии мышки уширяться ;), а при покидании сжиматься...
Не претендуя на изыски можно, например, вот так организовать галлерею:

  1. <StackPanel Orientation="Horizontal">
  2.       <Image Source="Chrysanthemum.jpg"></Image>
  3.       <Image Source="Desert.jpg"></Image>
  4.       <Image Source="Hydrangeas.jpg"></Image>
  5.       <Image Source="Jellyfish.jpg"></Image>
  6.       <Image Source="Koala.jpg"></Image>
  7.       <Image Source="Lighthouse.jpg"></Image>
  8.       <Image Source="Penguins.jpg"></Image>
  9.       <Image Source="Tulips.jpg"></Image>
  10.     </StackPanel>
* This source code was highlighted with Source Code Highlighter.


Ну а в случае, когда нам анимация изменения не нужна, можно применять "не евентовые" тригеры, которые отслеживают изменение свойтсв:

  1. <Style TargetType="{x:Type TextBox}">
  2.       <Setter Property="TextBox.Background">
  3.         <Setter.Value>
  4.           <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
  5.             <GradientStop Offset="0.0" Color="LightCyan" />
  6.             <GradientStop Offset="0.14" Color="Cyan" />
  7.             <GradientStop Offset="0.7" Color="DarkCyan" />
  8.           </LinearGradientBrush>
  9.         </Setter.Value>
  10.       </Setter>
  11.       <Style.Triggers>
  12.         <Trigger Property="IsFocused" Value="True">
  13.           <Setter Property="TextBox.Background">
  14.             <Setter.Value>
  15.               <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
  16.                 <GradientStop Offset="0.0" Color="LightSeaGreen" />
  17.                 <GradientStop Offset="0.14" Color="SeaGreen" />
  18.                 <GradientStop Offset="0.7" Color="DarkSeaGreen" />
  19.               </LinearGradientBrush>
  20.             </Setter.Value>
  21.           </Setter>
  22.         </Trigger>
  23.       </Style.Triggers>
  24.     </Style>
* This source code was highlighted with Source Code Highlighter.


Интерес представляют строки с 11 по 23. Как видно в строке 12 задается тригер который отслеживает состояние свойства IsFocused и как только оно станет равно True к TextBox-у будут применен новый Setter. Причем в данном случае заботиться об обратной смене значений не необходимости. При возврате свойства IsFocused в false значение Setter-а будет отменено и фон станет таким каким был по умолчанию.

среда, 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.

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

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