9/16/2011 5:41:00 AM

Прошёл ещё один день, а никто так и не умер. Нездоровая у нас комьюнити, всё время ждём смертей и кого-то хороним. То С++ хоронили, то то VB, то СОМ, то C#, то .NET. А никто так и не мрёт, не смотря и вопреки.

WinRT, вон, вполне себя через СОМ наружу показывает, чтобы всяческие писки моды вроде JavaScript могли его видеть и использовать. То же и с C#, кстати. В конечном итоге в unmanaged WinRT он смотрит через СОМ, только разработчику этого не видно. В WinRT существует специальный слой, который называется "language projections" (ещё его называют bindings), который отвечает механику вызовов API WinRT из соответствующих языков. Именно поэтому для разработчика что на С++, что на JavaScript общение с WinRT выглядит нативно.
Кроме этого WinRT обеспечивает автоматическую трансляцию типов для различных языков. Например, для коллекций внутри неуправляемого кода WinRT использует интерфейсы вроде IVectorView, строки типа HSTRING (они там свой тип строки сделали), но для, например, С#-разработчика это будет выглядеть как IList, string и т.д. Есть только два типа, которые не транслируются автоматически, но для них есть методы-расширения, которые это делают.

И

нфраструктура .NET, сам фреймворк, CLR существуют и отлично развиваются. Anders Hejlsberg сегодня представил новшества C# 5.0 и VB.NET 11, плюс рассказал (и показал) о некоторых планах на следующую версию (CTP Roslyn обещают через месяц!). Anders всё время забавно шутил по поводу статической и динамической типизации, например: "Вот, нам повезло, что JavaScript распознал этот тип! Естественно, в случае с С# нам везёт в 100% случаев..." или (показывая новую консоль C# Interactive) "Вот! И у нас здесь даже есть интеллисенс и возможности рефакторинга! Кстати, это всё потому, что статическая система типов.. ну да ладно!".

Ребята из Entity Framework показали новую версию (4.5). Новшеств тоже достаточно: поддержка перечислений, географических типов данных, миграторы (для обновления схемы БД), интеграция с database project (изменения одного отражаются на изменениях другого), возможность иметь больше одного файла модели, оптимизация запросов, автоматическая компиляция запросов и т.д.

Don Syme приехал презентовать F# 3.0, однозначно будет рассказывать о строготипизированных моделях данных для динамических источников данных, обязательно пойду завтра, будет интересно, очень занимательная фишка F# судя по уже годовой давности презентации идеи.

Целая сессия была про новшества .NET Framework 4.5, правда, я не ходил. Буду смотреть видео, судя по твиттеру сессия была интересная.

Ребята из команды Windows Azure AppFabric двигаются семимильными шагами. Хвастались, что заменили полностью архитектуру ServiceBus так, что никто и не заметил. Презентовали новые фишки вроде публикаций\подписок с различными вкусностями плюс большую кучу планов и идей на будущее.

Весёлый итальянский художник Витторио Берточчи рассказал и показал новшества Windows Identity Foundation и Access Management.

Всё живёт, всё развивается.
Я сейчас больше часа разговаривал с сотрудником Майкрософт, который в частности находится в комитете по развитию WinRT APIs, принимал участие в разработке Win8 с фазы спецификаций, он утверждает, что изначально стояла задача не убивать что-то, а использовать как можно более удачно и органично. Много чего интересного рассказал и объяснил, кстати.
Интересно сказал про "хоронильщиков": "Смотри сам. У нас есть миллионы строк кода приложений на .NET и Win32. И ноль строк кода приложений для нового API и стиля. Мы полагаем, что как писать .NET и Win32 приложения вы все представление имеете. А что такое мы новое сделали - нет. Так, если подумать, про что логичнее здесь говорить? Если мы не говорим много про что-то, это не значит, что оно умерло. Если у нас нет ни одной сессии про Windows Phone, то это не значит, что он умер. Команда там работает как машина над новым релизом! Это просто не их день, их здесь и нет никого, им и показывать-то может быть особо нечего!".

Интересно в конференции ещё и то, что много совершенно новых лиц. Объясняется это тем, что внутри Майкрософт очень ограниченый круг людей имел доступ к новым вещам. Более чем две трети разработчиков команды Виндовс до начала конференции не видели нового интерфейса. Никто из "евангелистов" его не видел. Так что весь Майкрософт, включая "обычных" публичных персон конференций такого уровня (вроде Хансельмана и иже с ним) точно так же только с началом конференции получил доступ к образу системы и точно так же скачивают и изучают, не взирая на чины, звания и заслуги. Будь это вице-президент или рядовой разработчик.
А те, кто выступают - как раз из команд, имеющих прямое отношение. Люди, которые обладают знаниями, но которых никто раньше не видел :)

А я сегодня получил сообщение на голосовую почту в отеле от самого же отеля. Длинное и вежливое, но если коротко, то что-то вроде "не забудь выписаться завтра". Всё, последняя ночь здесь. Завтра последний день конференции - и в самолёт. Даже немного жаль уезжать.  Говорят, что на этот раз таки будет А380.

9/14/2011 8:01:00 AM

Виндовс 7 официально побила по количеству установленных копий своего главного конкурента - Виндовс ХР!
Более 450 миллионов копий Виндовс 7 уже установлено в мире. За время её существования было сделано 1502 обновления (не имеющих отношения к секьюрити, именно улучшения)!
542 миллиона активных пользователей (те, которые периодически логинятся в систему) насчитывает сейчас Windows Live!

А мы возвращаемся к топику :)

Одна из важных новостей - Виндовс 8 завтра можно будет скачать. Developer Preview Edition x86 & 64. Никакой активации, но и никакого саппорта :) Кроме разработчиков система вряд ли кому-то будет интересна, ибо это ещё даже не бета. Впереди все стандартные фазы: Beta, RC, RTM, GA. Сроков не называли (Майкрософт давненько бросила называть сроки), по слухам RTM ожидают к февралю следующего года.

Виндовс 8 работает быстрее 7-ки и требует меньше памяти. В два раза меньше, если верить скриншотам (200 мегабайт против 400 для 7-ки).

Над производительностью тоже много и упорно работали, в результате система стала гораздо быстрее. В качестве доказательства был продемонстрирован старенький (два или три года назад купленый) нетбук с гигабайтом ОЗУ, Виндовс 8 чувствовала там себя неплохо. Из того, что было сделано:
- Серьёзно работали над производительностью системы в целом.
- Переписали XAML Engine полностью на нативный C++, что, собственно, позволяет приложениям на С++ его использовать, а вызовы объектов, описываемых XAML теперь просто компилируются как вызовы методов виртуальной таблицы, что, конечно, очень быстро.
- Ещё несколько пунктов, важных, важнее предыдущего, хотел написать, но вылетели дважды из головы! Вспомню - напишу.

Оставлена полная совместимость с Виндовс 7, как было заявлено - абсолютно полная, никаких здесь компромисов.

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

Приложения для Виндовс 8 как-бы поделили на две части: "обычные", и "метро-стайл". Про "обычные" никто ничего не говорит - они как были - так и остались, ничего (пока что) нового, только всё быстрее.
С метростайл приложениями всё интереснее. Виндовс 8 включает в себя целый новый стек для такого рода приложений. Снизу этого стека лежат сервисы ядра (которые так же используются и Вин32 инфраструктурой, и дотнетом, и сильверлайтом и т.д, но которые не включены в стек метро-стайл приложений). Поверх сервисов ядра лежит то, что называется Windows Runtime или, сокращённо, WinRT APIs. Это как раз та штука, о которой ходили слухи, что она будет являться заменой Win32 API. Насколько пока можно судить, так оно и есть.
Поверх WinRT располагаются языки программирования, которые могут работать с этими АПИ. Это могут быть в равной мере С++, C# или Visual Basic (и тогда в качестве разметки интерфейса используется XAML), либо же JavaScript (и тогда интерфейс делается на HTML5).

Вот на этом слайде видно, примерно, как это выглядит. Обратите внимание, что под WinRT APIs нет Win32.
http://community.devexpress.com/blogs/ctodx/Windows8Architecture_485458D4.png

Важно ещё раз отметить, что и XAML, и сам WinRT - компoненты "нативные". Если приложение делается на С++, то тут и вопросов никаких не возникает ни с производительностью, ни с.. как по-русски "интероперабилити"? :)
Под сишарпом и вижлбейсиком, однако, никакого дотнета нет. Он есть, но в другом месте, вне пределов метро-стайл приложений. Означает ли это, что сишарп и бейсик компилируются в нативный код? Я не знаю. С одной стороны, моно умеет это делать, почему бы и майкрософту не суметь? С другой стороны - мне в это не верится, особенно глядя на стоящий рядом кубик с javascript, который, вестимо, вообще никуда не компилируется. Очень надеюсь узнать подробности в ближайшие дни.

Не смотря на то, что WinRT "нативный", вызывать его из C# и даже Javascript не составляет никаких проблем. Обычное инстанцирование и использование нужного объекта, например, Camera.
Каждый API WinRT кроме библиотеки нативного кода содержит ещё и файл с метаданными. Физически это файл того же формата, в котором хранит метаданные CLR. Эта инфраструктура называется WinMD, а файлы, соответственно, MD-файлами. Они как-то используются для рефлексии как раз для того, чтобы WinRT могла быть использована отовсюду.
Как это конкретно работает у меня есть догадки, но пока писать не буду, подожду более точной информации.

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

В конце дня каждому выдали по две коробки (кстати, быстро разобрались с пятью тысячами участников, минут 10 всего в очереди стоять пришлось). В одной из коробок оказался планшетный компьютер Самсунг, в другой - синезубая клавиатура к нему и перо.
Диагональ планшета 11.6", экран - какой-то крутой самсунговский, который яркий и с углами обзора до 180 градусов. Экран не из пуленепробиваемого стекла, к сожалению, так что, наверное, нужна плёнка.
Процессор - Core i5 (ARM-то пока не готов), SSD на 64 гигабайта, симка с годовым обслуживанием по 2 гигабайта трафика в месяц (к сожалению, только на территории США). Имеется кредл и зарядник для планшета.

Планшет поддерживает до двух мониторов, на нём стоит предварительная версия Visual Studio 2011 и можно на нём же писать приложения для него же. Отлаживать код можно как прямо на планшете, так и в эмуляторе.
С эмулятором интересно, он работает через RDP к самому же планшету :)
С отладкой тоже интересно сделали в 2011-й студии: можно отлаживать приложение удалённо по сети, включая сеть беспроводную. То есть, с десктопной машины можно скомпилировать и запустить приложение на лежащем рядом планшете (по беспроводной сети), при этом работает всё, от интеллитрейса и всяких там отладочных прибамбасов и до DOM-инспектора с поиском элемента, на который укажут.

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

12/4/2010 5:19:27 AM

Доклад Эрика Майера о noSQL был лучшим на YOW!2010, по крайней мере из тех, что я видел. Уровень исполнения – выше всяких похвал. Одако, отдельно отметить его я хотел именно из-за его содержания.
Итак, краткая стенограмма.

Вначале Эрик сказал, что не так давно проходила конференция, посвящённая noSQL-системам, где был выдвинут важный тезис: мы знаем, что такое SQL, но мы до сих пор не знаем, что такое noSQL, поэтому мы бродим в потьмах и не знаем, что делать.
- Теперь у меня есть на это ответ. Я знаю, что такое noSQL и покажу вам. Больше нет никакого смысла противопоставлять SQL и noSQL, теперь наступит мир, сказал Эрик.

Для иллюстрации проблем, связанных с реляционными базами данных, он привёл простой пример:

public class Book {
    public string Title {get; set; }
    public IEnumerable<int> Ratings {get; set; }
    public IEnumerable<string> Authors {get; set; }
}
Итак, мы имеем объект, содержащий коллекции каких-то других объектов. Тут важно вспомнить, что слово “содержащий” фактически означает различный набор ссылок, то есть, фактически, Book ссылается на объекты рейтингов и авторов. Такой вот небольшой понятный граф, который мы хотим сохранить в БД.

Здесь начинаются “проблемы”, так как таблицы реляционных БД, могут хранить только скалярные значения. Иначе говоря, если описать в некой семантической форме, то значение в реляционной БД хранится в виде:

value = {scalar * scalar * scalar … } //так есть

и не может быть сохранено в, например, виде:

value = {scalar * value * scalar … } //а так – нету.

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

Далее он всячески “наезжал” на язык SQL (а что, как архитектор MS SQL Server он легко может себе такое позволить) за отсутствие возможности композиции, отсутствие возможности рекурсии как таковой (да, есть теперь CTE, но это не то), за то, что тип результата не соответствует типу запроса, много и интересно ссылался на различных мировых светил и т.д.

Наиболее эмоциональной частью выступления стала часть, посвящённая нормализации.
Эрик показал, что для сохранения экземпляра вышеуказанного типа Book необходимо провести нормализацию. То есть, в БД мы имеем для этого три таблицы: Books, Ratings и Authors (я не стану их рисовать, это легко представить). Каждая из этих таблиц имеет первичный ключ (некий ID). Две из этих таблиц имеют ссылки на первичный ключ первой (Foreign Keys).
Таким образом, заявил Эрик, из моего простого объекта получилось вдруг три новых, в которые добавились какие-то новые сущности (идентификаторы), не имеющие к реальному объекту никакого отношения, плюс нужно писать дополнительный код, осуществляющий трансформацию. “Нормализация?! – возмущался Эрик – Почему этот термин назван нормализация?! Вы правда считаете это нормальным, так делать?!” Smile

Посмотрим дальше на запросы к данным. Для того, чтобы получить список книжек с высоким рейтингом из коллекции Books, потребовался бы достаточно простой запрос, тогда как в случае “нормализованного” хранилища уже ридётся использовать join’ы, что приведёт к серьёзному усложнению решения простой задачи.

Ещё много всяких “косяков” связанных с самой идеей SQL и реляционных БД приводил Эрик, однако я перейду к сути “проблемной” части, почти словами Эрика:

  1. Я создаю мой прекрасный тип Book. Он простой, понятный и с ним легко работать.
  2. Приходит другой человек и “портит” мой тип, создавая вместо него три других, пихая туда всякие неотносящиеся к делу IDs и т.д.
  3. Приходит БД-человек, и начинает решать обратную задачу, как-бы воссоздавая из всего этого “нормализованного” вида то, что у меня уже и так было в самом начале!

На пункте #3 становимся чуть подробнее. Все эти join’ы и IDs – они служат только для того, чтобы поддерживать целостность некого логического объекта Book. Сервер БД внутри себя содержит алгоритмы немереной сложности для поддержки целостности данных с помощью foreing key – только для того, чтобы обеспечить целостность объекта Book. Индексы. Что делают индексы? Фактически, это пре-подготовленные справочники того, как одни таблицы связаны с другими, то есть, опять же, служащие для того, чтобы быстро иметь возможность понять, какая запись в таблице Ratings “принадлежит” какой записи в таблице Books.
То есть, все эти умные механизмы служат одной и той же цели: восстановить то, что я имел в пункте #1 после того, как это было испорчено в пункте #2.

Ну а теперь непосредственно к делу Smile

- Меня на курсе математики научили только одной вещи, я в жизни знаю только один трюк. Это трюк – двойственность, пошутил Эрик.

Если обратить внимание на то, чем отличаются две эти модели друг от друга, то можно заметить самое главное: в исходном “ссылочном” подходе “родитель” так или иначе “знает” о своих потомках (имеет на них  ссылки). В результирующем “реляционном” подходе наоборот - потомок всегда знает о том, кто его родитель. Вот здесь Эрик и заприметил возможность применить свой любимый трюк.
То есть, чтобы получить одно из другого, нужно всего лишь, выражаясь его языком, “развернуть стрелочки”!

- Наша отрасль называется “computer science”. Вы заметили, что все “настоящие” науки не имеют слова “science” в своём названии? Химия, физика, математика… Поэтому когда в нашем “science” возникает проблема, я знаю, что скорее всего она уже решена в “настоящей” науке и пришло время обратиться к математике. Я открыл гугл и набрал слова “математика развернуть стрелочки”. Ба! Википедия! Теория категорий и ко-алгебра! Теперь я точно знаю, что есть решение проблемы!

Erik Meijer: Математики уже решили всё за нас

Так Эрик описал процесс Smile

Ну а дальше они засели, как он выразился, с “настоящими математиками”, чтобы доказать действительность двойственности SQL и noSQL. И у них это получилось. И теперь Эрик предлагает называть это не noSQL, а coSQL, в соответствии с математическими стандартами, как co-alghebra.

Erik Meijer об SQL: Узнаёте монады?

Таким образом они доказали, что все законы и свойства, применимые к SQL-системам, дуально применимы к coSQL системам и наоборот. Взять ту же ссылочную целостность:
- в SQL ссылки направлены от родителя к детям, в coSQL – от детей к родителям;
- в SQL используется “внешняя” системы ссылок (объекты маркируются ID), в coSQL – внутренняя система (ссылки на сами объекты);
И так много, много пунктов.

Что из этого следует?
А то, что SQL и coSQL могут сосуществовать в одной экосистеме (и то, что этим занимается архитектор MS SQL Server уже является кое-каким намёком), а не являются “непримиримыми врагами”.
То, что (и Эрик это даже прямо сказал) обе эти системы могут иметь некий общий “интерфейс” и взаимодействие с системой (язык, если хотите) можно построить на основе этого “интерфейса”, то есть, одинаково работать и с тем и с другим.
То, что можно использовать наработки из одной системы в другой. Например, в SQL-системах имеются прекрасные оптимизаторы запросов и ещё куча всяческих полезных алгоритмов – вероятно, их можно будет применить в coSQL.
То, что, в конце концов, системы являются трансформируемыми друг в друга (и это следует напрямую из математики).

На мой взгляд – очень стоящее открытие. Публика тоже была в восторге.

10/20/2010 3:12:47 PM

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

Поясню на примере.
Допустим, у нас есть:

//здесь мы получаем номер   
IObservable<int> numbers; 

 //функция, генерирующая набор простых чисел в диапазоне от 0 до value   
public IObservable<int> GetPrimes(int value) {...}

И задача стоит простая: для каждого полученного в numbers числа “выдавать” набор простых чисел в диапазоне от 0 и до этого числа.

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

Обобщаются все эти задачи одним: в процессе обработки элемента нужно “включить в результат” некую другую последовательность.

Казалось бы, чего проще! В LINQ эта задача давно решена с помощью SelectMany и решение выглядит следующим образом:

//для каждого элемента выдавать последовательность, возвращаемую
//функцией GetPrimes(...)
var result = numbers
    .SelectMany(x=>GetPrimes(x));

И это будет работать! С одной оговоркой: это будет работать не совсем так, как нам нужно. Следующая марбл-диаграмма объясняет принцип работы SelectMany в Rx (на примере вышеуказанной конструкции):

marble

Как видно, последовательность “numbers” генерирует некие значения (синие точки). Синие точки сами по себе не попадают на зелёную линию (результат) потому, что мы обрабатываем их с помощью .SelectMany, “производя” для каждого элемента новую последовательность с помощью метода GetPrimes(…). Последовательности изображены красной и жёлтой линиями. И вот уже их результаты попадают на зелёную линию Result. Но попадают они туда тоже асинхронно, в том же “темпе”, в котором функция GetPrimes их генерирует. То есть, результаты “путаются”, их порядок не гарантирован и не детерминирован.
Можно рассматривать эту ситуацию как если бы мы просто подписались на несколько независимых последовательностей (что в сущности и происходит).

Мы же хотим получить несколько иную картину: синяя точка + весь набор красных. Следующая синяя точка + весь набор жёлтых. И так далее.

Добиться этого можно с помощью преобразования “вложенной” последовательности в IEnumerable (благо библиотека Rx определяет методы для таких преобразований).
И тогда мы можем описать в общем виде функцию, позволяющую добиться искомого результата:

public static IObservable<U> ProduceMany<T, U>(this IObservable<T> source, Func<T, IObservable<U>> function)
{
    return source
        .SelectMany(x => function(x).ToEnumerable());
}

Сигнатура функции точно такая же, как и у SelectMany (монадный Bind, однако Winking smile), а воспользоваться ею теперь можно вот так:

using (numbers
            .ProduceMany(x=>GetPrimes(x))
            .Subscribe(Console.WriteLine))
{
    Console.ReadKey();
}

Вот столько вот текста – и в конце функция на одну-две строчки.
Ну, просто хочется, чтобы было понятно… Smile

10/12/2010 4:43:34 PM

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

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

//раз существует Enumerable.Range, то почему бы не быть Observable.Range, подумали создатели Rx
var
digits = Observable.Range(1, 10).Do(ThrowIf5);

Функция .Do(Action), которую я не показывал раньше, просто производит какое-то действие (побочный эффект) над каждым элементом и просто возвращает исходный Observable. В моём случае это функция, выкидывающая исключение при значении элемента равном 5, как и следует из её названия.

Самый простой способ обрабатывать исключения – это не обрабатывать их вообще. Этот способ мы рассматривать не будем.

Второй по простоте способ – это перехватить исключение “в конце”, на этапе получения результата. Как я уже показывал, в Rx это можно сделать в момент подписки:

digits.Subscribe(
    Console.WriteLine, 
    e=>Console.WriteLine(e.Message) //подписываемся на исключение, которое придёт в OnError
);

Такой способ означает, что клиент, подписывающийся на данный аспект, должен как-то отреагировать на наличие ошибки. Это правильно, так как, в реальной жизни, гарантировать “безошибочность” невозможно.
Кроме того, подписываясь на что-то клиент не знает (и не должен знать) того, как это “что-то” устроено и, следовательно, всегда должен предполагать возможность появления ошибки и пути её обработки.

Однако, далеко не все исключения имеет смысл доводить до клиента. Некоторые из них могут быть вполне успешно обработаны “по пути” к клиенту. Возможность такой обработки в Rx предоставляет метод .Catch(…), который можно читать как “В случае возникновения ошибки продолжить с…”:

//в случае любой ошибки перейти к другому observable
digits = digits.Catch(Observable.Range(-5, 6));

Или так:

//а теперь перехватываем только ArgumentException
digits = digits.Catch((ArgumentException e) => Observable.Range(-5, 6));

В качестве результата мы увидим (клиент получит) значения 1,2,3,4,-5,-4,-3,-2,-1,0.

Не смотря на простоту (и благодаря возможности композиции) .Catch(…) может играть очень значительную роль в построении алгоритмов.
Часто встречающаяся ситуация: “если не получилось – попробовать ещё раз” конечно может быть обработана клиентом с помощью метода .Subscribe(…). Но как только мы начинаем об этом думать, так сразу встают вопросы от “как подписаться на ошибку так, чтобы повтор был только однократный” и до “а почему, собственно, клиент должен каждый раз об этом заботиться”? Попробуйте представить себе такое решение.

А вот гораздо более удобный способ:

//при возникновении любой ошибки просто вернуть тот же observable ещё один раз (повторить)
digits = digits.Catch(digits);

Коментарий – и тот получился длиннее Smile

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

public static IObservable<T> WaitFor<T>(this IObservable<T> source, TimeSpan time)
{
    return Observable.Timer(time).SelectMany(source);
}

Одна строчка кода, однако, возможно, требует пояснений.
Когда я столкнулся с этой задачей (в моём случае это было не просто “подождать”, а “спросить у сервера что происходит”, я рассуждал так: Мне нужно дождаться результата из одного Observable и только потом перейти к другому. Использовать метод .Concat(…) для конкатенации результатов двух аспектов я не могу: во-первых, их результаты разных типов, а во-вторых, даже если я как-то трансформирую результат первого во второй (с помощью метода .Select(…) например, хотя как можно сделать манго из огурца?), то я нне хочу, чтобы этот трансформированный результат попал клиенту! Мне важен факт того, что ожидание завершилось, а результат – нет.
И тут я вспомнил недавнюю беседу о монадах и “вырожденных методах bind” Smile И подумал: а ведь и верно. Мне ведь просто нужен какой-то метод с сигнатурой “M a –> (a –> M b) –> M b”!
То есть, мне нужен метод Bind!
Точнее даже мне нужен метод “M a –> (_ –> M b) –> M b” (подчёркивание означает, что значение меня не интересует и имя ему присваивать я не намерен). То есть, при любом значении я возвращаю M b – и всё. Т
о есть, долой функцию и получаем M a –> M b –> M b.
То есть, мне даже лучше иметь “вырожденный” метод Bind Smile
Далее, что у нас является методом Bind в монаде LINQ? Да это же .SelectMany(…)!
Rx определяет .SelectMany(…) для IObservable. Более того, оказалось, что Rx определяет и “вырожденный” случай, тоже.

Вот так и получается с .SelectMany(…): Дождаться результата, игнорировать его и вернуть то, что передано в параметре.
Так же и WaitFor: создаётся Observable, который получит новое значение в OnNext через промежуток времени time, этот результат будет игнорирован и SelectMany просто вернёт тот аспект, который нам нужен после ожидания.
Надеюсь, понятно объяснил.

Ну а дальше дело техники:

//в случае ошибки повторить через три секунды
digits = digits.Catch(digits.WaitFor(TimeSpan.FromSeconds(3)));

Примерно таким же образом я решал задачу “если вдруг возникло исключение “session timeout”, то сходить на сервер, получить новую сессию, а потом повторить запрос”. Абсолютно то же самое, только запрос к веб-серверу из предыдущего постинга вместо таймера.

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

public static IObservable<T> TryMultipleTimes<T>(this IObservable<T> response, int maxTries)
{
    return response.TryMultipleTimes<T>(0, maxTries);
}

//метод приватный, так как мы не хотим, чтобы текущее значение счётчика было доступно извне
private static IObservable<T> TryMultipleTimes<T>(this IObservable<T> response, int i, int maxTries)
{
    if (++i < maxTries)
        return response.Catch(response
                                .WaitFor(TimeSpan.FromSeconds(.25))
                                            .TryMultipleTimes(i, maxTries));

    return response;
}  

Использовать, опять же, очень просто:

digits = digits.TryMultipleTimes(3);

Ну и в заключение хочу отметить ещё один метод: .Finally(Action).
Как видно из названия, этот “обработчик” срабатывает как в случае OnComplete, так и в случае OnError.
Использование его и вовсе тривиально:

digits = digits
    .TryMultipleTimes(3)
    .Finally(() => Console.WriteLine("We've done!"));

 

P.S. В этом постинге я ничего не сказал о методе Observable.Throw<TResult>(Exception ex). Но он есть. И он “производит” observable указанного типа, сразу вызывая в нём OnError и передавая в него указанное исключение. Ой, сказал Smile

10/10/2010 12:07:16 PM

Я не знаю, как вам, но мне лично (да и не только мне) работа с шаблоном IAsyncResult всегда доставляла головную боль. Все эти BeginOperationName, EndOperationName, Callbacks… Обработка ошибок там…
Нет, я не говорю что IAsyncResult – это плохо. Это хорошо и полезно и всё такое, просто работать с этим достаточно сложно.

Было сложно, пока не появился Rx Winking smile

Что нам нужно чтобы, например, асинхронно получить данные с веб-сервиса или веб-странички?
Да практически ничего:

   1:  public static class WebRequestExtensions
   2:  {
   3:      public static IObservable<WebResponse> ToObservable(this WebRequest request)
   4:      {
   5:          if (request == null) throw new ArgumentNullException("request");
   6:   
   7:          var getResponse = Observable
   8:              .FromAsyncPattern<WebResponse>(request.BeginGetResponse, request.EndGetResponse);
   9:   
  10:          return getResponse();
  11:      }
  12:   
  13:      public static string GetContentAsString(this WebResponse response)
  14:      {
  15:          if (response == null) throw new ArgumentNullException("response");
  16:   
  17:          using (var stream = response.GetResponseStream())
  18:          using (var reader = new StreamReader(stream, false))
  19:              return reader.ReadToEnd();
  20:      }
  21:   
  22:  }

Вся “магия” заключена в методе ToObservable(..), конкретно – в строках 7 и 8.
Вот и всё! Это действительно всё, что нам нужно сделать для работы с IAsyncResult. Следующий метод – GetContentAsString(..) – просто вспомогательный метод, позволяющий получить строку ответа из потока, я привёл его просто потому, что буду его использовать дальше в примере:

   1:  public static IObservable<string> GetObservableWebContent(string url)
   2:  {
   3:      var request = WebRequest.Create(new Uri(url));
   4:   
   5:      return request
   6:          .ToObservable()
   7:          .Select(x => x.GetContentAsString());
   8:  }

Итак, теперь для любого URL мы можем получить IObservable, который будет “уведомлен” когда результат будет готов. Причём результат мы уже имеем в виде строки, то есть, он готов к обработке.

Для начала просто выведем его на экран:

   1:  public static void PrintContentAsync(string url)
   2:  {
   3:      var observable = GetObservableWebContent(url)
   4:          .Subscribe(x => { Console.WriteLine(x); });
   5:  }

Но стоп, что если URL неправильный? Или сервер не отвечает? Или происходит ещё что-то неожиданное?
Тогда нам просто нужно сделать обработку ошибок:

   1:  public static void PrintContentCatchError(string url)
   2:  {
   3:      GetObservableWebContent(url)
   4:          .Subscribe(
   5:              x => { Console.WriteLine(x); },
   6:              e => { throw e; });
   7:  }

Здесь я в строке 6 передаю Action-обработчик исключения. Я-то просто выкидываю это исключение, но вы-то можете поступить как угодно! Более умно, например Smile
Библиотека Rx содержит метод Catch<TException>(..), позволяющий обработать исключение без разрывания цепочки (например, можно  попробовать отследить таким образом какой-нибудь SessionTimeout, возобновить сессию и повторить попытку).

Но что если, даже имея такой вот замечательный observer я всё же хочу выполнить операцию синхронно? Что если мне нужно здесь и сейчас дождаться получения результата, заморозить UI, остановить время и мир до тех пор, пока ценные данные не будут получены?
В Случае с IAsyncResult я мог просто сделать что-то вроде: var result = EndOperationName(BeginOperationName(…));

Но и здесь всё гораздо проще (строка номер 4):

   1:  public static void PrintContentSync(string url)
   2:  {
   3:      var content = GetObservableWebContent(url)
   4:          .Single();
   5:   
   6:      Console.WriteLine(content);
   7:  }

Словом, при наличии IObservable жизнь становится намного проще и легче, чем при наличии просто event’ов, IAsyncResult’ов и т.д.
Причём немаловажно то, что когда у меня есть IObservable я просто пишу свою бизнес-логику и мне уже совершенно не важно, что там было изначально: пара-тройка event’ов ли (как в случае с перетаскиванием объекта из предыдущего постинга), IAsyncResult ли, что-то ещё или всё это скомбинированное вместе…

10/10/2010 11:00:28 AM

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

Итак, Rx – это библиотека для .NET Framework, полное название которой звучит как “Reactive Extensions”. Reactive не потому, что “круто, как реактивный самолёт”, а потому, что…

Существует два варианта взаимодействия со средой: проактивный (интерактивный) и реактивный. Мы знакомы с обоими:

  • Проактивный – это когда мы предпринимаем какое-то действие над средой для получения результата (мы задаём кому-то вопрос, мы наливаем чай в кружку, мы вызываем метод GetNewPosts(lastDate)). Мы активно производим какие-то действия, чтобы получить от среды какой-то результат (pull-взаимодействие, мы “вытягиваем” что-то из среды).
  • Реактивный – это когда мы реагируем на какие-то события, происходящие вокруг (кто-то звонит нам по телефону и мы отвечаем на звонок, официант приносит нам чай и мы отвлекаемся от созерцания океана чтобы его поглотить, срабатывает обработчик OnMouseClick). То есть, в среде постоянно происходят какие-то события, на которые мы можем (а иногда и должны) реагировать. Среда сама предоставляет нам информацию о произошедшем событии (push-взаимодействие).

Нетрудно догадаться, что Rx посвящена этому самому реактивному (push) взаимодейтсвию.

Создатели Rx обратили внимание на интерфейсы IEnumerable и IEnumerator, заметив, что эти два интерфейса олицетворяют собою проактивный (интерактивный) подход.

А лучше рисовать я и не умею!

Действительно, во что вытекает работа с IEnumerable/IEnumerator (и всеми их “наследниками”)?

   1:  IEnumerable<object> enumerable;              //где-то есть у нас такое
   2:   
   3:  var enumerator = enumerable.GetEnumerator(); //запрашиваем enumerator
   4:  ...
   5:  var moved = enumerator.MoveNext();           //передвигаем "курсор"
   6:  var element = enumerator.Current;            //запрашиваем текущий элемент
   7:  ...
   8:  enumerator.Dispose();                        //"убиваем" enumerator

Да, какой-нибудь foreach делает за нас всю эту работу, от получения IEnumerator и до его dispose, но суть в том, что мы всё время “дёргаем” что-то, чтобы получить результат: проактивное взаимодействие.

Дальше создатели Rx подумали: поскольку реактивное взаимодействие является как бы “зеркальной копией” проактивного, что если сделать зеркальные копии IEnumerable/IEnumerator?
IEnumerator позволяет нам передвигать курсор, изменяя текущее значение? Тогда среда должна иметь возможность “уведомить” его “зеркальную копию” о том, что текущее значение изменилось (где-то кем-то).
IEnumerable позволяет нам получить экземпляра IEnumerator? Тогда его “зеркальная копия” должна дать нам возможность предоставить среде “зеркальную копию” IEnumerator’а, как бы заявив: “вот та сущность, которая будет реагировать на данный аспект.

Если присмотреться, то видно: все эти “зеркальные копии” представляют собой шаблон “Observer”, который описан GoF миллионы лет наза.
“Зеркальной копией” IEnumerator может являться нечто с именем IObserver, а “зеркальную копию” IEnumerable тогда назовём IObservable.

public interface IEnumerator<T> : IDisposable
{
    T Current { get; }
    bool MoveNext();
    void Reset();
}
public interface IObserver<T>
{
    void OnCompleted();
    void OnError(Exception exception);
    void OnNext(T value);
}

Рассмотрим, что представляет собой это “зеркалирование”:

Вызывая IEnumerable.MoveNext() мы перемещаем курсор, после чего получаем текущее значение из IEnumerable.Current.
Значит, IObserver должен иметь метод IObserver.OnNext(currentValue), позволяющий нам реагировать на изменение значения (получить это изменённое значение).

Кроме того, когда мы “делаем” IEnumerable.MoveNext(), метод возвращает нам результат этой операции: true, если курсор переместился и false, если мы дошли до конца.
Значит, среда должна иметь возможность уведомить IObserver о том, больше “обозревать” нечего. Таким образом нам понадобится метод IObserver.OnCompleted().

Ещё один аспект при работе с IEnumerable.MoveNext() состоит в том, что если в процессе перемещения курсора возникает исключение, то код, вызывающий MoveNext(), его тут же и получит. Что, естественно, невозможно в случае IObserver, так как в этом случае мы не контроллируем среду. Если при вычислении нового значения где-то в среде произойдёт исключение, у среды не будет возможности вызвать IObserver.OnNext(currentValue) просто потому, что этот самый currentValue не был вычислен.
Поэтому для уведомления об ошибках используется метод IObserver.OnError(exception).

Глядя на всё это лично я считаю, что интерфейс IObserver даже более “прозрачен” и понятен, чем IEnumerator. Вон, сколько всего скрыто за одним только MoveNext(), в то время, как IObservable имеет все эти аспекты разложенными по полочкам.

public interface IEnumerable<T>
{
    IEnumerator<T> GetEnumerator();
}
public interface IObservable<T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

Как уже говорилось, IObservable позволяет нам предоставить среде некую реализацию IObserver, которая будет реагировать на уведомления среды. Делается это, как видно, с помощью метода IObservable.Subscribe(observer).

Тут необходимо отметить важный момент: IObservable содержит метод Subscribe(observer) но не содержит метода Unsubscribe(observer). Означает ли это, что единожды подписавшись, мы обречены на эту подписку навсегда? Конечно же нет.
IObserver не содержит метода Unsubscribe по ряду простых причин, объединённых одним корнем: а не удобно это ни фига! Пришлось бы “таскать с собой”, либо хранить где-то тот самый observer, чтобы потом иметь возможность передать его в метод Unsubscribe.

Вместо этого IObservable.Subscribe(observer) возвращает нечто, реализующее IDisposable. Это а) является “зеркальным отражением” того, что IEnumerable наследует IDisposable, и б) вызов Dispose как раз и означает “нам больше не нужна эта подписка, отписываемся!”.

Забегая вперёд скажу, библиотека Rx автоматически “отписывает” observer “когда это нужно”: после OnCompleted и OnError, поэтому явное отписывание нужно достаточно редко, но всё же нужно, мы увидим это дальше.

Собственно, идея реализации шаблона Observer методом “зеркального отражения” шаблона Enumerator настолько же проста, насколько и гениальна. Наверное поэтому Эрик Мейер (человек, который когда-то придумал LINQ и который теперь придумал Rx) сказал: “когда я смотрю на это, я понимаю, что мне можно уходить на пенсию, потому, что ничего столько же красивого я в своей жизни придумать уже не смогу” Smile

Итак, понятно, что существует два интерфейса IOvservable<T>/IObserver<T> (кстати, в .NET 4.0 эти интерфейсы уже включены в BCL), но что же всё-таки позволяет нам делать библиотека Rx?

О, много чего.
Для начала, Rx содержит множество возможностей получения IObservable из различных источников.

Вот так, например, можно получить IObservable из события:

var observable = Observable.FromEvent<MySpecialEventArgs>(myObject, "MyEventName");

Использование имени события - не единственная возможность, Observable содержит несколько перегрузок FromEvent(..).

А дальше.. Rx реализует великое множетво методов для работы с IObservable. Rx реализует LINQ для IObservable!

Вот пример кода, который возвращает изменения координат объекта при перетаскивании его с помощью мыши:

   1:  public static IObservable<Point> GetMouseDrag(this UIElement uiElement)
   2:  {
   3:      return GetMouseMove(uiElement)
   4:                  .SkipUntil(GetMouseLeftButtonDown(uiElement).Do(o => uiElement.CaptureMouse()))
   5:                  .TakeUntil(GetMouseLeftButtonUp(uiElement).Do(o => uiElement.ReleaseMouseCapture()))
   6:                  .Let(mm => mm.Zip(mm.Skip(1), (prev, cur) =>
   7:                      new Point
   8:                      {
   9:                          X = cur.EventArgs.GetPosition(uiElement).X - prev.EventArgs.GetPosition(uiElement).X,
  10:                          Y = cur.EventArgs.GetPosition(uiElement).Y - prev.EventArgs.GetPosition(uiElement).Y
  11:                      }))
  12:                  .Repeat();
  13:  }
  14:   
  15:  public static IObservable<IEvent<MouseEventArgs>> GetMouseMove(this UIElement uiElement)
  16:  {
  17:      return Observable.FromEvent<MouseEventArgs>(uiElement, "MouseMove");
  18:  }
  19:   
  20:  public static IObservable<IEvent<MouseButtonEventArgs>> GetMouseLeftButtonDown(this UIElement uiElement)
  21:  {
  22:      return Observable.FromEvent<MouseButtonEventArgs>(uiElement, "MouseLeftButtonDown");
  23:  }
  24:   
  25:  public static IObservable<IEvent<MouseButtonEventArgs>> GetMouseLeftButtonUp(this UIElement uiElement)
  26:  {
  27:      return Observable.FromEvent<MouseButtonEventArgs>(uiElement, "MouseLeftButtonUp");
  28:  }

Код собственно логики здесь заключён в методе GetMouseDrag(..), который возвращает наружу IObservable<Point>.
Эта конструкция начинает предоставлять данные как только пользователь нажмёт левую кнопку мыши (SkipUntil просто игнорирует все перемещения до этого момента) и завершится тогда, когда пользователь её отпустит (за это отвечает TakeUntil).
Инструкция “Repeat()” в конце означает, что при получении OnCompleted() нужно снова подписаться на то же самое ещё раз.
Если мы подпишемся на этот IObservable, то мы просто будем получать значения “объект сместился на столько-то пикселей” тогда, когда происходит собственно перетаскивание:

var dragObservable = GetMouseDrag(myImage);
dragObservable.Subscribe(delta => { Console.WriteLine("dX: {0}, dY: {1}", delta.X, delta.Y); });

Поскольку эта конструкция (с помощью инструкции Repeat()) каждый раз “самоподписывается” заново, мы можем осуществлять такое перетаскивание неоднократно и всегда получать изменения координат. Если же нам нужно в какой-то момент перестать это делать, то, как я уже говорил, нужно просто вызвать Dispose у объекта, который возвращает .Subscribe(..), либо просто заключить его в using.

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

P.S. Скачать Rx для .NET 3.5, .NET 4.0, Silverlight 3, Silverlight 4 и даже для JavaScript (!) можно на домашней страничке проекта: http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx

6/7/2010 3:06:47 PM

По поводу предыдущей заметкипро FlowChart.

Как совершенно правильно заметил Паша в баззе, проблема всего этого великолепия с “долгоиграющими” workflow заключается в том, их нельзя менять.

При попытке что-то добавить или удалить запущеные и сохранённые в БД экземпляры просто не будут оттуда загружаться, а Workflow Runtime будет выдавать страшное сообщение о том, что “дерево Activities изменено, и поэтому состояние не может быть восстановлено”.

Как это решается? А никак. Просто никак. Официально предложен следующий механизм решения конфликта: името параллельно несколько версий и настроить роутинг WCF-сообщений. Чтобы сообщения, содержащие контракт “v1” доставлялись на сервис V1, а сообщения, содержащие контракт “v2” доставлялись на сервис V2.

В случае, если Ваш контракт физически не менялся, предложено внести в него какой-нибудь признак, по которому можно будет сориентироваться.

Далее предлагается поступить так, как в КВН у команды Астаны (Казахстан) было: “Ну их!” :)
То есть, “старые” процессы дорабатывают по старой схеме, а те, которые начинаются новые – идут по схеме новой.

Иногда это не лишено смысла, допускаю. Однако, что делать, если нужно банально исправить ошибку? Особенно, если у нас имеются “бесконечные” workflow, как, например, документы в системе: они просто переходят из одного статуса в другой, туда, обратно, раз в два месяца…
Ответа на этот вопрос, видимо, не существует :)

Как вариант можно попробовать (только мысли, сам не ковырялся) написать свой вариант сохранения/загрузки workflow в/из БД.
Делается это, вроде бы, совершенно не сложно: надо просто создать своего наследниеп System.Runtime.DurableInstancing.InstanceStore. А вот насколько будет сложно там, внутри, разобраться, что данные у нас от V1 и построить на основе них V2 – непонятно, надо пробовать :) Наверное, однозначно ответить нельзя: когда-то это сделать легко, а когда-то и невозможно просто потому, что данных не хватит…

Пример того, как сделать собственную “сохранялку” в XML есть в библиотеке примеров для WF и WCF, здесь.

Паша ещё предложил как вариант делать stateless workflows. Чтож, вариант, хотя и очень-очень сильно компромисный и не всегда приемлемый. К тому же, в WF4 нет State Machines, и как “сказать” Workflow, что сейчас она находится не вот в этом состоянии, а вон в том – я не знаю. Допустим, она “висит” на Delay где-нибудь в середине. Или на ждёт, что кто-то “дёрнет” WCF-метод.

По поводу сохранения ещё, раз уж Паша эту тему тоже затронул.
В WF4 это тоже переделано относительно 3.5. Теперь Workflow Runtime не сохраняет сам Instance, а сохраняет только некие его состояния, переменные, в результате чего объёмы получаются гораздо меньше, нежели они были раньше. Что, в принципе, правильнее и логичнее. Но вот поднять именно код из сохранённого состояния больше не выйдет.

По поводу ошибок теперь.
В WF4 есть activities для транзакций и обработки ошибок (try..catch). То есть, всё можно делать непосредственно там – о отлов, и компенсацию, и обработку.
Если же всё-таки где-то недосмотрели и “выпали” в событие UnhandledException, то в этом случае мы “теряем” только то состояние, которое находилось на момент ошибки в памяти. То есть, в следующий раз workflow будет продолжена с момента последнего сохранения в БД.
Кстати, есть там ещё замечательная Persist Activity. Понятно, что она делает ;) Так вот, сначала Persist, потом делаем перевод денег :) Если что-то не получилось – в следующий раз выполнение продолжится с того же самого места, как и было указано в Пашином примере.

Но с версионностью – засада, конечно. Надо ещё будет попробовать, возможно ли её разрешать автоматически хоть в простых случаях… Тогда, может быть, комбинация stateless (or minimum state) + такая вот собственная сохранялка хоть как-то сможет помочь…

6/6/2010 2:39:02 PM

Два дня потратил на то, чтобы более-менее разобраться в Workflow Foundation 4-й версии.

Из новых замечательных возможностей – FlowChart диаграмма и возможность выставить содержащую её Workflow в сеть в виде “обычного” WCF-сервиса.

Сама по себе FlowChart диаграмма очень удобна, прежде всего своей наглядностью (сразу понятно, как протекает процесс), а так же тем, что практически в точности отражает те блок-схемы, которые вырисовываются на досках и бумажках при продумывании схемы работы. Наиболее счастливые разработчики сразу получают задание от специалиста по требованиям в виде примерно такой вот схемы с фразой “я хочу, чтобы было так!” в заголовке :)

Программисту остаётся только открыть Visual Studio, выбрать там проект, который называется “WCF Workflow Service Application”, и начать определять свой сервис в файле Service1.xamlx (лучше переименовать, или создать новый ;))

Я попробовал сделать небольшой сервис по обработке tickets (извините, я буду писать “тикетов”, так как не сумел придумать нормальный перевод).
Система примерно такая: тикет регистрируется в системе, после чего может изменяться до тех пор, пока не перейдёт в состояние Cancelled или Closed.
При этом он может попасть в состояние WaitingForCustomer. В этом состоянии он может ожидать неделю. Если клиент ничего не ответил, то тикет закрывается.

В результате получается что-то вроде вот такой диаграммы:

FlowChart 

Блок “Receive and Update Ticket” – это просто Activity, определённая в отдельном .xaml-файле.
Он отвечает за то, чтобы получить тикет посредством WCF-метода, сохранить его где-то, возможно, изменив, и отправить изменённый тикет обратно (для отображения).
Выглядит это так:

 Получение тикета через сервис

Здесь я просто “перетянул” мышкой в дизайнере ReceiveAndSendReply Activity, в результате чего образовались два блока, отмеченные стрелками. Первый – это получение WCF-сообщения, второй – возвращение значения.
Между получением значения и возвращением результата можно делать какие-то действия. Любые, в общем-то. Можно туда ещё один workflow вставить, который будет что-то важное делать, подготавливая данные к отправке, например, ещё одну FlowChart диаграмму :)
В моём случае всё предельно просто: я только добавляю запись в коллекцию History у тикета и возвращаю его обратно. Хотя, неплохо было бы сначала изменить историю, а потом уже сохранить ;)
Понятно, что если значение возвращать не надо, то можно воспользоваться просто Receive Activity.

Важно здесь то, что прямо в этих avtivities указывается имя метода (ProcessTicket), тип контракта, который будет принимать/получать метод (или можно указать кучку параметров) и что этот метод будет возвращать (это в SendReplyToReceive). Всё остальное будет сделано автоматически, метод станет виден WCF-клиентам как самый обычный WCF-метод.

Конфигурация WCF-метода.

Здесь MessageType = Ticket – это тип принимаемого сообщения (я принимаю Ticket), в Message data = _ticket – это имя переменной, куда я хочу положить принятый результат.

А ещё, поскольку мы получаем данные, производим над ними работу, а потом отправляем результат назад, нам нужно обеспечить для каждого такого процесса свой собственный, выделенный экземпляр workflow. Сделать это просто: нужно в свойствах Receive Activity включить галку CanCreateInstance. Впрочем, если Вы забудете это сделать, то при запуске Вам напомнят, да.
Итак, после этого у нас каждый тикет будет обрабатываться собственным экземпляром workflow. Что нам, собственно, и надо, ибо жизненные пути тикетов различны…

LogTicketInfo Activity я описывать не буду, скажу только, что это просто класс, activity, унаследованная от CodeActivity и она не делает ничего полезного, кроме Debug.WriteLine(..).
В WF4 предполагается наследоваться от CodeActivity и CodeActivity<T> для выполнения каких-то программных действий, либо от NativeActivity и NativeActivity<T>, если нужно как-то при этом взаимодействовать с Workflow Runtime. Generic-версии используются в том случае, если нужно вернуть какой-то результат заданного типа.

Блок, отвечающий за ожидание ответа клиента в течение недели выглядит так:

Ожидание ответа клиента в течение 7 дней

Тут я использовал стандартную Pick Activity для выбора одного из “направлений движения”. Их у меня всего два: либо клиент ответил, и тогда ничего делать не надо (анализируем новый статус как обычно), либо клиент НЕ ответил в установленный срок, и тогда закрываем тикет.

Кстати, для получения ответа клиента я использую ту же самую activity, что в самом начале, то есть, в моём случае, используется тот же самый WCF-метод. Что уже, в принципе, должно навести на мысли… ;)

Ну вот, а после получения тикета и записи лога (см. flowchart, первую картинку) всё стекается в блок Switch по полю Status тикета, который и решает, что мы должны делать дальше.

Казалось бы – должно работать! И если мы запустим проект и откроем любимый WcfTestClient.exe, то увидим, что оно и работает! Но если поиграться чуть дольше (или догадаться чуть раньше), то станет понятно, что работает оно неправильно.

Неправильно по двум причинам:

  1. Все Workflow, находящиеся в статусе отличном от Cancelled или Closed, висят в памяти неопределённое время (пока ресурсы не кончатся).
  2. Сколько бы мы ни пытались сказать, что тикет такой-то переходит из состояния WaitingForCustomer в любое другое – мы всё время попадаем в начальный блок “Receive And Update Ticket”, а не в тот, что находится в “Wait For Customer”. Иначе говоря, каждое обращение к методу у нас фактически активирует новый процесс, так как наш сервис не понимает, что это тот же самый тикет продолжает свой жизненный путь, а не просто новый “с неба свалился”. Таким образом, экземпляры всё копятся и копятся в памяти.

Самое время решить эти проблемы.

Первую решить довольно-таки просто и очевидно: воспользоваться стандартным persistence service, который уже есть в WF4. Сделаем так, что если workflow не активна, то она сохраняет себя в базе данных SQL Server и не занимает места в памяти и прочих ресурсов. Там, в базе, она может лежать годами, и никому от этого плохо не будет.
Для этого нам всего лишь нужно подготовить таблицы в БД, выполнив последовательно вот эти два (отмечены галочками, в том же порядке) скрипта:

Схема БД

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

Далее нужно просто пойти в web.config файл и прописать там следующие строки (понятное дело, строка подключения к БД у каждого своя):
Web.Config

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

Хорошо, что в .NET 4.0 всё, что только можно, уже прописано в “базовые” конфигурационные файлы :) Все конфигурационные файлы стали на порядок легче. Поэтому он уже “знает”, что такое эти два тега и может их использовать.

Со второй проблемой несколько сложнее.
Она ведь в чём состоит, проблема-то? Состоит она в том, что для обработки каждого тикета Workflow Runtime создаёт отдельный экземпляр workflow, но когда в метод ProcessTicket приходит “тот же самый” тикет, Workflow Runtime снова создаёт экземпляр, вместо того, чтобы использовать уже существующий.
Почему это происходит? Да потому, что Workflow Runtime просто не знает, что значит “тот же самый”. Как только она научится это знать – так сразу всё встанет “на свои места”.

Такой процесс “узнавания” называется “кореляцией” (correlation). Иными словами, мы должны провести кореляцию между нашим тикетом и экземпляром workflow.
Делается это тоже несложно: в Workflow для этого предусмотрен специальный тип переменной, называемый CorrelationHandle.

Так, как я использовал ReceiveAndSendReply Activity, то он создал мне такую переменную автоматически.
Я не буду тут отвлекаться на различные аспекты кореляции, (коих несколько, и есть дажа пара activities на данную тему), это отдельный разговор, здесь мы просто решим “насущную” задачу: укажем, что кореляция проводится по идентификатору тикета.

Для этого достаточно пойти в свойства Receive Activity, кликнуть там поле CorrelatesOn и указать нужный нам correlation handler (был создан автоматически, смотрите список переменных) и параметр для кореляции (Id):

Кореляция по полю Id 

В результате получится что-то такое:

Опять кореляция

Это означает, что когда в наш сервис в метод ProcessTicket приходит сообщение, то происходит его анализ по полю, указанному вот в этом вот XPath выражении, что было построено автоматически. Если значение этого выражения совпадает с каким-либо экземпляром workflow, то это экземпляр будет “поднят” из базы данных и это сообщение и будет обрабатываться именно этим экземпляром, что нам и нужно было сделать :)
Поэтому, когда в экземпляр, ждущий ответа от клиента, приходит такое сообщение, он продолжает свою работу именно с блока “Жду клиента”, а не попадает в начало процесса.

И, вроде бы, всё должно работать. И всё будет работать! :) И всё будет хорошо!

Но недолго. Кто знает, до каких пор? ;)

11/18/2009 9:52:42 PM

Интересно здесь. Не то, чтобы совсем, но неожиданно.

Майкрософт объявили направление: веб-приложения, которые (одинаково) доступны на лаптопах, десктопах и мобильных устройствах.
Основной платформой (по крайней мере для UI) объявлен Сильверлайт.

И вот тут начинается интересное, с анонса Silverlight 4.
Silverlight 4 содержит очень много замечательных вещей, начиная с drag-n-drop и заканчивая тем, что теперь он может работать вне пределов браузера, как нормальное приложение.
При этом его можно назначить trusted – и он может иметь доступ к железу, файловой системе, другим веб-сайтам и сервисам, да мало ли.
Например, внутри сильверлайта можно запускать всякие другие вещи. В качестве примера – adobe flash. Ребята запускали youtube на демо.

В общем, похоже, сильверлайт придётся осваивать.

P.S.
Сижу сейчас на секции, посвящённой будущему программирования. Докладчики – их пятеро – рассуждают.. о чём бы вы думали? О распараллеливании, о том, что в функциональных языках этих проблем нет и о том, как с этим быть :) Проблема серьёзно назрела.

Powered by BlogEngine.NET 2.5.0.6

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter


Recent posts

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2012

Sign in