вторник, 9 октября 2012 г.

Покер, LINQ и конечные автоматы.

 

Архив с проектом, описанным в этой статье находится здесь
Pocker.rar

Задумался над решением задачи распознавания покерных комбинаций. С одной стороны человек довольно легко распознает каждую комбинацию, хотя вариантов здесь – море, с другой - как это решить программно? Не перебирать же в конце концов все варианты для каждой комбинации?

Немного поразмыслив, нашел решение. Набор из пяти карт можно рассматривать как множество(коллекцию) и применив некоторые операции с множествами можно получать разные результаты, в зависимости от того, с какой комбинацией мы имеем дело. Собственно LINQ-запросы я не использовал, однако методами класса System.Linq.Enumerable пользовался активно, отсюда слово LINQ в заголовке записи.

Итак, в чем суть подхода. Выполняя некую операцию над множеством, можно получить результат, свойственный одним комбинациям, но совершенно не свойственный другим и таким образом сузить круг поиска. Дальнейшие операции(уже другие) позволят уточнить результат и с каждым таким шагом круг поиска будет сужаться пока не придем к однозначному решению. Ветвление логики проще всего изобразить графически в виде набора состояний, при достижении каждого из которых можно будет точно сказать, что данная комбинация является одной из соответствующих этому состоянию и не может быть никакой другой. Финальные состояния соответствуют одной комбинации каждое.Поскольку графическое представление более наглядно и понятно, я использовал для решения задачи рабочие процессы (Workflow). В дизайнере рабочих процессов есть конечный автомат, который и позволит нам реализовать логику распознавания.

Для начала надо подготовиться и создать класс карты и перечисления, соответствующие масти, достоинству и комбинации.

VB.Net
  1. Public Class Card
  2.     Public Sub New(rank As CardRank, flush As Flush)
  3.         Me._rank = rank
  4.         Me._flush = flush
  5.     End Sub
  6.  
  7.     Private _rank As CardRank
  8.     Public ReadOnly Property Rank() As CardRank
  9.         Get
  10.             Return _rank
  11.         End Get
  12.     End Property
  13.  
  14.     Private _flush As Flush
  15.     Public ReadOnly Property Flush() As Flush
  16.         Get
  17.             Return _flush
  18.         End Get
  19.     End Property
  20.  
  21.     Property RandomOrder As Integer
  22.  
  23.     Public Overrides Function ToString() As String
  24.         Return String.Format("{0} Of {1}", Me.Rank, Me.Flush)
  25.     End Function
  26. End Class
  27.  
  28. Public Enum Flush
  29.     '
  30.     Spades
  31.     '
  32.     Clubs
  33.     '
  34.     Diamonds
  35.     '
  36.     Hearts
  37. End Enum
  38.  
  39. Public Enum CardRank
  40.     Ace = 1
  41.     C2
  42.     C3
  43.     C4
  44.     C5
  45.     C6
  46.     C7
  47.     C8
  48.     C9
  49.     C10
  50.     Jack
  51.     Queen
  52.     King
  53. End Enum
  54.  
  55. Public Enum Combination
  56.     ' .
  57.     HighCard
  58.     Pair
  59.     TwoPair
  60.     [Set]
  61.     Straight
  62.     Flush
  63.     FullHouse
  64.     Quads
  65.     StraightFlush
  66.     RoyalFlush
  67. End Enum

Далее добавляем в проект рабочий процесс( если изначально это не был выбран шаблон проекта, в котором он уже есть).  С панели элементов добавляем конечный автомат в дизайнер рабочего процесса (StateMachine) и можно начинать колдовать. Результатом моих усилий стала схема, имеющая следующий вид

StateMachine

Вначале мы попадаем в состояние Инициализация набора. Здесь инициируются исключения для случаев, когда набор не соответствует нормам. У нас есть один входной аргумент Hand, представляющий набор карт, в котором надо узнать комбинацию и если коллекция не существует (аргумент не передан), состоит не из пяти карт или все пять карт одного достоинства(в колоде их может быть только четыре) – создается исключение. Можно было бы еще предусмотреть варианты, когда встречаются совпадающие карты( в колоде обычно нет повторений), но это смысла в нашей задаче иметь не будет, в силу того, что на логику распознавания комбинаций не влияет, а вот вышеперечисленные условия должны выполняться, иначе иначе все будет работать со сбоями.

Щелкнув дважды по состоянию мы входим в его структуру. В поле Entry переносим действие If, в поле Condition которого вводим следующий код

Hand IsNot Nothing AndAlso Hand.Count <> 5 AndAlso DistinctCount > 4

В поле Then переносим действие Throw, а в его свойстве Exception пишем

New ArgumentException("Входной аргумент Hand должен быть массивом, содержащим 5 карт.")

На этом первичная инициализация завершена и можно приступать к реализации логики.


Первым критерием, который можно применить к набору, может быть проверка масти. Есть комбинации, в которых все карты состоят из одной карт масти, а есть такие, в которых масти различны. Таким образом, проверив все ли карты одной масти мы сразу разобьем все возможные комбинации на две группы, в одной будут Flush, Straight Flush и Royal Flush; а в другой – все остальные.Для каждой из этих групп мы добавим по состоянию(Онда масть и Разные масти) и соединим их с уже существующим (Инициализация набора). Чтобы проверить все ли карты одной масти, надо проверить масть любой(например первой) карты, после чего посчитать сколько карт в наборе имеют такую же масть. Посчитать можно с помощью функции Count класса Enumerable, расширяющую все коллекции. Поскольку проверку эту придется проводить для обоих переходов в следующее состояние, лучше результат сохранить в переменной(переменную можно создать, кликнув слово “Переменные” в нижнем левом углу дизайнера). Я назвал ее FlushCardCount и инициировал выражением

Hand.Count(Function(c) c.Flush = Hand(0).Flush)

То есть подсчитываются те карты, у которых свойство Flush имеет то же значение, что и это же свойство у первой карты.


Теперь выделяем переход от состояния Инициализация набора к состоянию Одна масть и в свойствах пишем


DisplayName = “5 карт одной масти
Condition = “FlushCardCount = 5


С переходом к состоянию Разные масти надо сделать то же, только там переменная не должна равняться 5, ну и текст перехода тоже должен быть соответствующим.


Если у нас одна масть, то возможны три варианта комбинаций: Flush, FlushStright или RoyalFlush.


Начнем с RoyalFlush. Эта комбинация возникает, когда мы имеем карты: туз, десятка валет дама и король. При этом замечаем, что карты с 2 по 9 отсутствуют. Кроме того, учитывая, что мы разбираем вариант игры с одной колодой, можно сказать точно, что если в наборе из пяти карт одной масти отсутствуют карты с двойки по девятку, то это RoyalFlush, поскольку в колоде 13 карт одной масти и если 8 из них отсутствуют, а остальные не повторяются( это условие выполняется благодаря тому, что мы имеем одну колоду, стало быть быть двух карт одной масти и одного достоинства быть не может), значит эти пять карт и есть остальные 5 карт данной масти. Таким образом нам нужно проверить, что карты с 2 до 9 отсутствуют. Результат проверки наличия карт этого диапазона мы сохраним в переменную, поскольку аналогичную проверку придется проводить и для комбинации Straight и нет смысла делать это дважды.


Создаем переменную NonBetween1And10 и инициируем ее выражением




NonBetween1And10



  1. Hand.Count(Function(c) c.Rank > CardRank.Ace AndAlso c.Rank < 10) = 0





Теперь добавляем элемент FinalState, соединяем его с состоянием Одна масть, в условии перехода указываем NonBetween1And10. Меняем DisplayName на RoyalFlush.


Теперь нам нужно создать выходной параметр Result, который будет выводить найденную комбинацию. Открываем раздел с параметрами, указываем имя Result, направление – выходной, тип – Combination. После этого раздел с параметрами можно закрыть.


Дважды щелкнув по состоянию RoyalFlush перетащим в поле Entry действие Assign. В левом поле укажем Result, в правом – Combination.RoyalFlush. Таким образом, если наш процесс дойдет до этого состояния, то завершившись он возвратит именно это значение.


Для комбинаций FlushStraight и Flush рассмотрим конкретный пример, ну скажем (2,3,4,5,6). В данном случае, если мы вычтем из максимального числа минимальное, то получится 4. Учитывая, что карты не повторяются(одна масть одной колоды), можно точно сказать, что если эта величина равна 4-м, то это FlushStraight, в ином случае – просто Flush. Таким образом нам надо ввести переменную RankRange и инициировать ее выражением




RankRange



  1. Hand.Select(Function(c) c.Rank).Max - Hand.Select(Function(c) c.Rank).Min





Далее добавляем еще два финальных состояния, соединяем их с “Одной мастью”, в условиях проверяем значения RankRange на равенство четырем, меняем названия и добавляем присвоение значение параметру Result, аналогично тому, как это было сделано для комбинации RoyalFlush.


Теперь перейдем к состоянию Разные масти. Если в наборе разные масти, то все такие комбинации можно разделить на две группы: комбинации, в которых присутствуют карты одного достоинства и те, в которых достоинства не повторяются. То есть следующим шагом будет добавление еще двух состояний, соответствующих указанным случаям. Проверить повторяемость достоинств мы можем с помощью функции Distinct, которая удаляет повторяющиеся элементы из коллекции, стало быть применив эту функцию можем просто проверить изменилось ли количество элементов, если да – значит есть повторы.


Создадим переменную RankCount и инициируем ее выражением




RankCount



  1. Hand.Select(Function(c) c.Rank).Distinct().Count





Эта переменная показывает количество достоинств в наборе. Ну то есть, если весь набор состоит из двоек пятерок и королей, то эта переменная будет иметь значение – 3.


Ну и соответственно для перехода к состоянию Есть повторы эта переменная должна быть меньше 5, а для перехода к состоянию Нет повторов – равняться 5.


Если повторов нет, значит это либо Straight, либо HighCard (проигрышная комбинация).Таким образом к состоянию Нет повторов добавляем два финальных состояния с именами соответствующих комбинаций, каждое из которых будет инициировать параметр Result соответствующим значением. Для определения условий перехода в эти состояния у нас уже есть готовые переменные (RankRange и NonBetween1And10), так что нам нужно только проверить их значения. В условии перехода к Straight пишем




Straight



  1. RankRange = 4 OrElse NonBetween1And10





Ну и соответственно в условии перехода к HighCard пишем





HighCard



  1. RankRange <> 4 AndAlso Not NonBetween1And10






Теперь рассмотрим ситуацию, когда не все карты одной масти и есть повторы. Самым простым случаем здесь будет комбинация Pair. Добавляем для нее конечное состояние и в условии перехода пишем




Pair



  1. RankCount = 4





То есть, если количество достоинств в коллекции равно 4, это значит, что мы имеем дело с одним повтором, то есть присутствует одна пара карт одного достоинства, все остальные разные.


Создадим еще одну переменную и назовем ее MaxRankCount. Эта переменная будет отображать наибольшее количество карт одного достоинства. То есть для каре ее значение будет равно 4, а для фуллхауса – 3. Выражение, которым мы инициируем эту переменную будет иметь следующий вид.




MaxRankCount



  1. Aggregate c As Card In Hand Select Hand.Count(Function(x) x.Rank = c.Rank) Into Max()







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


Нетрудно понять, что Quads – единственная комбинация, для которой эта величина будет равна 4. По этой причине сразу можно добавить финальное состояние и в условии перехода к нему выполнить проверку




Quads



  1. MaxRankCount = 4





Для двух пар эта переменная будет иметь такое же значение как и для пары, но зато переменная RankCount здесь будет иметь значение 3, а не 4.




TwoPair



  1. MaxRankCount = 2 AndAlso RankCount = 3





А вот для FullHause и Set можно создать еще одно промежуточное состояние с условием




FullHouse или Set



  1. MaxRankCount = 3





Однако RankCount для FullHouse будет равно 2, а для Set – 3, что и следует отобразить в условиях перехода к финальным состояниям. В каждом финальном состоянии выходной параметр Result инициируется соответствующим состоянию значением. И на этом построение рабочего процесса завершено и можно переходить к коду.


Для того, чтобы можно было работать с этим рабочим процессом в коде, его вызов надо обернуть в функцию. В моем проекте класс рабочего процесса называется CombinamionTester и код выглядит следующим образом.




GetCombination



  1. Public Function GetCombination(hand As Card()) As Combination

  2.     Dim tester = New CombinationTester()

  3.     Dim res = WorkflowInvoker.Invoke(tester, New Dictionary(Of String, Object)() From {{"Hand", hand}})

  4.     Return res("Result")

  5. End Function





Функция принимает массив из пяти карт и возвращает элемент перечисления Combination.


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


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




CreateHand



  1. Function CreateHand(ParamArray intvalues() As Integer) As Card()

  2.     Return New Card() {

  3.          New Card(intvalues(0), intvalues(1)),

  4.          New Card(intvalues(2), intvalues(3)),

  5.          New Card(intvalues(4), intvalues(5)),

  6.          New Card(intvalues(6), intvalues(7)),

  7.          New Card(intvalues(8), intvalues(9))

  8.          }

  9. End Function





Данная функция создает массив из пяти карт и для этого ей надо передать набор из десяти чисел, каждое из которых соответствует элементам перечислений Flush и Rank. В аргументах они идут попарно, то есть если я хочу, чтобы первой картой был туз пик, я передаю первые два аргумента – 1 и 0, так как 1 соответствует тузу в перечислении Rank, а ноль – пикам в перечислении Flush. Следующие восемь чисел следуют тому же принципу.




TestCombination



  1. Sub TestCombination(comb As Combination, ParamArray ints() As Integer)

  2.     Dim hand = CreateHand(ints)

  3.     Dim testedcomb = GetCombination(hand)

  4.     Assert.AreEqual(comb, testedcomb, String.Format("{0}   {1}.", comb.ToString, testedcomb.ToString))

  5. End Sub





Метод TestCombination тестирует одну комбинацию. Первый аргумент задает ожидаемый результат, а второй – набор чисел, передаваемый функции CreateHand и следующий той же логике. В этом наборе карты должны составлять ту же комбинацию, что описывает первый аргумент метода.


Ну и собственно сам код теста




TestGetCombinationMethod



  1. <TestMethod()> Public Sub TestGetCombinationMethod()

  2.      TestCombination(Combination.HighCard, 1, 0, 3, 1, 5, 0, 7, 2, 9, 3)

  3.      TestCombination(Combination.Pair, 1, 0, 1, 1, 5, 0, 7, 2, 9, 3)

  4.      TestCombination(Combination.TwoPair, 1, 0, 1, 1, 5, 2, 5, 3, 9, 3)

  5.      TestCombination(Combination.Set, 1, 0, 1, 1, 1, 2, 5, 3, 9, 3)

  6.      TestCombination(Combination.Set, 5, 3, 9, 3, 1, 0, 1, 1, 1, 2)

  7.      TestCombination(Combination.FullHouse, 1, 0, 1, 1, 1, 2, 5, 3, 5, 2)

  8.      TestCombination(Combination.Quads, 1, 0, 1, 1, 1, 2, 1, 3, 9, 3)

  9.      TestCombination(Combination.Flush, 1, 0, 3, 0, 4, 0, 7, 0, 10, 0)

  10.      TestCombination(Combination.StraightFlush, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0)

  11.      TestCombination(Combination.RoyalFlush, 1, 1, 10, 1, 11, 1, 12, 1, 13, 1)

  12.      TestCombination(Combination.Straight, 7, 0, 8, 2, 9, 3, 10, 1, 11, 1)

  13.      TestCombination(Combination.Straight, 1, 3, 10, 0, 11, 2, 12, 3, 13, 1)

  14.  

  15.  End Sub





Тест проходит успешно. Первоначально у меня были ошибки (в основном из-за невнимательности), но благодаря информативности теста и тому, что логика разветвлена визуально, я прямо по сообщению об ошибке очень быстро нашел ее. Кроме того, в процессе написания статьи я несколько изменил схему автомата по сравнению с первоначальной, но и это далось намного легче, чем поиск ошибок в коде. Все-таки визуальное проектирование оказалось весьма полезным.

воскресенье, 2 сентября 2012 г.

DXCore и автоматизация Visual Studio

Итак, стала доступна Visual Studio 2012. Казалось бы впору радоваться, однако что-то радоваться совсем не хочется. Установив новую версию, я, естественно, в первую очередь полез посмотреть на любимые фичи, ну там изучить новые возможности и все такое. Учитывая, что возможности редактора кода в студии не безграничны(мягко говоря) и зачастую необходимо их расширить, то в связи с этим одной из моих любимых возможностей студии была возможность писать макросы. На самом деле инструмент просто незаменимый: большинство отсутствующих возможностей студии можно добавить, просто слегка отвлекшись на написание макроса. Написал и он тут же доступен, запускай и пользуйся. Можно, конечно писать надстройки, но вся прелесть макросов в том, что они доступны сразу после написания, да и в отличие от надстройки, макро-проект не нужно продумывать заранее, а достаточно писать макросы по мере возникновения необходимости в той или иной функции студии. Когда скопится достаточно, макро-проект можно и в надстройку преобразовать, благо со студией поставлялись примеры макросов, среди который есть и такой, который выполняет преобразование макро-проекта в проект надстройки. Сделать это совсем не сложно, учитывая, что и макросы и надстройки используют одну и ту же объектную модель автоматизации.

Расстроившись по поводу нововведения, я решил поискать решение среди расширения для студии. Макросы, конечно же, заменить трудно, но можно расширить возможности студии. Из инструментов, действительно заслуживающих внимания помимо всем хорошо известного Resharper’а, можно еще выделить Visual Assist X и CodeRush. Последний привлек мое внимание наличием бесплатной версии, поэтому с нее я и начал.

CodeRush Xpress включает в себя множество возможностей, помогающих удобно писать код. Помимо описания в документации на официальном сайте можно почитать еще здесь о возможностях этого плагина для студии. Кое-что из описанного не будет работать, например из-за версии, сейчас изменились клавиши быстрого доступа и кое-какие другие вещи. клавиши быстрого доступа есть в документации и в окне опций, но вот с ними-то как раз в бесплатной версии и проблема. При загрузке студии, когда интерфейс еще не доступен, можно увидеть меню “DevExpress”, которое исчезает во время загрузки. В окне настройки меню и это меню тоже присутствует, но активировать его не получится. Связана такая скрытность с какими-то лицензионными ограничениям, одним словом : майкрософт потребовала от DevExpress не включать меню  в бесплатную версии по крайней мере по умолчанию. Требование странное, но тем не менее это так и делается. Решение проблемы описано здесь. Версия платформы, для которой это решение описано устарела, но метод с реестром работает, то есть надо просто внести в нужную ветвь параметр. В той версии, которая установлена у меня ветвь называется
HKEY_LOCAL_MACHINE\SOFTWARE\Developer Express\CodeRush for VS\11.2
Последняя ветвь названа по номеру версии, так что в дальнейшем тоже будет меняться. Ну, а параметр, который надо установить такой же
"HideMenu"=dword:00000000.
После этого, если студия запущена, то ее надо перезапустить для того, чтобы изменения вступили в силу.

Помимо возможностей самого CodeRush Xpress меня заинтересовало другое. Данный продукт выполнен на ядре DXCore и производитель любезно предоставляет API для написания собственных плагинов, которые можно встраивать в ядро и пользоваться всеми его возможностями. Краткое описание, что есть DXCore, можно найти здесь.

Создание плагина.

Не смотря на то, что кое-какую документацию, статьи и записи в блогах по этому вопросу найти можно, тем не менее, документация крайне скудна. Вот например сайт, посвященный документации по DXCore
https://sites.google.com/site/dxcoredocs/contents
А вот описание работы с RefactoringProvider
https://sites.google.com/site/dxcoredocs/plugin-components/Creating-a-RefactoringProvider
Конечно, понять как начать работать можно и по этому документу, но, судя по всему, придется до всего доходить методом “научного тыка”.

Здесь я опишу пример создания плагина, добавляющий в оператор Select (switch в C#) Case’ы для каждого элемента какого-нибудь перечисления. Штуковина востребованная, да и в полной версии CodeRush она вроде бы есть.

Вначале все так, как описано по последней ссылке:
Создаем проект( в диалоге выбираем в ветке DXCore проект плагина).
На поверхность дизайнера бросаем RefactoringProvider, и устанавливаем значения для необходимого минимума свойств

  • Name: SwitchEnumExpander
  • DisplayName: Case для перечисления(это текст, который будет отображаться как пункт меню)
  • ProviderName: SwitchEnumExpander

Кроме того, будет совсем не лишним добавить описание того, что делает  эта функция. Для этого надо установить значение для свойства Description, в моем случае это был следующий текст.

Если оператор switch (Select в Visual Basic) проверяет значение выражения, возвращающего перечисляемый тип (Enum), команда добавляет в него операторы case для всех значений перечисления.

Он будет отображаться, когда соответствующее меню выделено.

Далее вставляем в код обработчики событий CheckAvailability и Apply. В обработчике первого события мы проверяем, доступен ли наш рефакторинг в данном контексте, а второй будет выполняться в том случае, если пользователь выберет соответствующий пункт меню. То есть основное действие выполняется именно в обработчике события Apply нашего провайдера.

Теперь о коде. Для начала объявим три поля для хранения объектов, которые нам помогут в построении кода.

VB.Net
  1. Dim _codeModServices As New CodeModServices
  2. Dim _sourceTreeResolver As New SourceTreeResolver()
  3. Dim _elementBuilder As New ElementBuilder

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

Первый объект CodeModeServices нам понадобится именно для генерации кода.

SourceTreeResolver нужен для получения информации о типах. Когда мы найдем имя типа возвращаемого выражением , значение которого проверяет наш оператор Select, то по имени нам надо будет получить информацию о типе. Но тут есть трудности, в проекте могут быть определены собственные типы, кроме того, он может ссылаться как на другие проекты, так и на готовые сборки. В текущем файле могут быть импортированы пространства имен, кроме того, они обычно импортируются и на уровне проекта( в бейсике). Таким образом, среди всего этого многообразия нам потребуется отыскать нужный тип и проверить, является ли он перечислением, а если да – получить имена определенных в нем  констант. Для решения этих вопросов нам и понадобится экземпляр класса SourceTreeResolver.

ElementBuilder помогает строить дерево кода.

Для начала определим функцию, которая будет находить информацию о типе перечисления, если таковой имеется и доступен в текущем контексте.

VB.Net
  1. Function FindEnumForSwitch(sw As Switch) As IElement
  2.     Dim typename = sw.Expression.GetDeclaration.ToLanguageElement.GetTypeName
  3.     Dim typeel = _sourceTreeResolver.ResolveTypes(sw, typename, True)
  4.     Return Aggregate te As IElement In typeel Where te.ElementType = LanguageElementType.Enum Into FirstOrDefault()
  5. End Function

Когда какой-нибудь элемент Select будет доступен, мы будем передавать его этой функции, для того, чтобы определить, возвращает ли проверяемое им выражение объект перечисляемого типа (если нет – функция будет возвращать Nothing) и если да, то мы получим сведения об этом перечислении.

Далее, в обработчике события CheckAvailability нам нужно дать ответ на вопрос о доступности нашего рефакторинга в данном контексте. Доступным он будет при соблюдении двух условий: элементом контекста является оператор Select и проверяемое им выражение возвращает значение перечисляемого типа(последнее обстоятельство проверим с помощью только что написанной функции FindEnumFromSwitch).

VB.Net
  1. Private Sub SwitchEnumExpander_CheckAvailability(sender As System.Object, ea As DevExpress.CodeRush.Core.CheckContentAvailabilityEventArgs) Handles SwitchEnumExpander.CheckAvailability
  2.     If ea.Element.ElementType = LanguageElementType.Switch Then
  3.         ea.Available = FindEnumForSwitch(ea.Element) IsNot Nothing
  4.     End If
  5. End Sub

Теперь добавим в Select Case’ы. Для этого создадим отдельную функцию, причем она будет получать эелемент Select взятый из кода, а возвращать его копию, заполненную кейсами. Можно было бы добавлять кейсы непосредственно в этот же селект, но мы не будем этого делать, поскольку эту функцию мы будем использовать и для показа превью, а поскольку окно превью может появляться неоднократно – лучше все-таки исходный селект не трогать.

VB.Net
  1. Function CreateSwitchWithEnumCases(switch As Switch) As IElement
  2.     Dim langelcoll As New LanguageElementCollection
  3.  
  4.     Dim enumType = FindEnumForSwitch(switch)
  5.     Dim casestmt = Function(ie As IElement)
  6.                        Dim cs = _elementBuilder.BuildCase(_elementBuilder.BuildMemberAccessReference(enumType.FullName,
  7.                                                                           ie.Name, MemberAccesOperatorType.Default))
  8.                        cs.AddNode(_elementBuilder.BuildBreak)
  9.                        Return cs
  10.                    End Function
  11.  
  12.     langelcoll.AddRange((From ie As IElement In FindEnumForSwitch(switch).Children
  13.                         Where ie.ElementType = LanguageElementType.EnumElement
  14.                         Select casestmt(ie)).ToList)
  15.     Dim result As Switch = switch.Clone
  16.     result.RemoveNodesFromIndex(0)
  17.     result.AddCaseStatements(langelcoll)
  18.     Return result
  19. End Function

Здесь при генерации кейса мы добавляем оператор break, в бейсике это не нужно, но в других языках(а плагин будет работать не только в бейсике) это необходимо.

Теперь когда у нас появилась функция, генерирующая результат можно реализовать окошко preview, для того, чтобы пользователь мог видеть какой код будет сгенерирован еще до того, как примет изменения. Для этого нам потребуется обработать событие PreparePreview нашего провайдера. Код довольно прост.

VB.Net
  1. Private Sub SwitchEnumExpander_PreparePreview(sender As System.Object, ea As DevExpress.CodeRush.Core.PrepareContentPreviewEventArgs) Handles SwitchEnumExpander.PreparePreview
  2.     ea.AddCodePreview(ea.Element.View.GetSelectionRange.Start, _codeModServices.GenerateCode(CreateSwitchWithEnumCases(ea.Element)))
  3. End Sub

Ну и все что осталось – это только вставить код в том случае, если пользователь выбрал соотвествующий пункт меню. Для этого нужно обработать событие Apply провайдера.

VB.Net
  1. Private Sub SwitchEnumExpander_Apply(sender As System.Object, ea As DevExpress.CodeRush.Core.ApplyContentEventArgs) Handles SwitchEnumExpander.Apply
  2.     Dim sw = CType(ea.Element, Switch)
  3.     Dim typename = sw.Expression.GetDeclaration.ToLanguageElement.GetTypeName
  4.     Dim typeel = _sourceTreeResolver.ResolveTypes(sw, typename, True)
  5.  
  6.     sw.SelectCode()
  7.     ea.Selection.Text = _codeModServices.GenerateCode(CreateSwitchWithEnumCases(sw))
  8.     ea.Selection.Clear()
  9. End Sub

Здесь для вставки кода я воспользовался сгенерированным текстом, выделил блок Select и заменил выделенный текст новым, после чего свернул выделение. Скорей всего это не самый оптимальный способ замены кода, но как это делать правильно, я пока не знаю, а этот способ вполне себе сносно работает.

В заключение приведу полный код плагина.

VB.Net
  1. Imports System.ComponentModel
  2. Imports System.Drawing
  3. Imports System.Windows.Forms
  4. Imports DevExpress.CodeRush.Core
  5. Imports DevExpress.CodeRush.PlugInCore
  6. Imports DevExpress.CodeRush.StructuralParser
  7. Imports System.Linq
  8.  
  9. Public Class PlugIn1
  10.  
  11.     'DXCore-generated code...
  12. #Region " InitializePlugIn "
  13.     Public Overrides Sub InitializePlugIn()
  14.         MyBase.InitializePlugIn()
  15.  
  16.         'TODO: Add your initialization code here.
  17.     End Sub
  18. #End Region
  19. #Region " FinalizePlugIn "
  20.     Public Overrides Sub FinalizePlugIn()
  21.         'TODO: Add your finalization code here.
  22.  
  23.         MyBase.FinalizePlugIn()
  24.     End Sub
  25. #End Region
  26.  
  27.     Dim _codeModServices As New CodeModServices
  28.     Dim _sourceTreeResolver As New SourceTreeResolver()
  29.     Dim _elementBuilder As New ElementBuilder
  30.  
  31.     Private Sub SwitchEnumExpander_Apply(sender As System.Object, ea As DevExpress.CodeRush.Core.ApplyContentEventArgs) Handles SwitchEnumExpander.Apply
  32.         Dim sw = CType(ea.Element, Switch)
  33.         Dim typename = sw.Expression.GetDeclaration.ToLanguageElement.GetTypeName
  34.         Dim typeel = _sourceTreeResolver.ResolveTypes(sw, typename, True)
  35.  
  36.         sw.SelectCode()
  37.         ea.Selection.Text = _codeModServices.GenerateCode(CreateSwitchWithEnumCases(sw))
  38.         ea.Selection.Clear()
  39.     End Sub
  40.  
  41.  
  42.     Function CreateSwitchWithEnumCases(switch As Switch) As IElement
  43.         Dim langelcoll As New LanguageElementCollection
  44.  
  45.         Dim enumType = FindEnumForSwitch(switch)
  46.         Dim casestmt = Function(ie As IElement)
  47.                            Dim cs = _elementBuilder.BuildCase(_elementBuilder.BuildMemberAccessReference(enumType.FullName,
  48.                                                                               ie.Name, MemberAccesOperatorType.Default))
  49.                            cs.AddNode(_elementBuilder.BuildBreak)
  50.                            Return cs
  51.                        End Function
  52.  
  53.         langelcoll.AddRange((From ie As IElement In FindEnumForSwitch(switch).Children
  54.                             Where ie.ElementType = LanguageElementType.EnumElement
  55.                             Select casestmt(ie)).ToList)
  56.         Dim result As Switch = switch.Clone
  57.         result.RemoveNodesFromIndex(0)
  58.         result.AddCaseStatements(langelcoll)
  59.         Return result
  60.     End Function
  61.  
  62.     Private Sub SwitchEnumExpander_CheckAvailability(sender As System.Object, ea As DevExpress.CodeRush.Core.CheckContentAvailabilityEventArgs) Handles SwitchEnumExpander.CheckAvailability
  63.         If ea.Element.ElementType = LanguageElementType.Switch Then
  64.             ea.Available = FindEnumForSwitch(ea.Element) IsNot Nothing
  65.         End If
  66.     End Sub
  67.  
  68.     Function FindEnumForSwitch(sw As Switch) As IElement
  69.         Dim typename = sw.Expression.GetDeclaration.ToLanguageElement.GetTypeName
  70.         Dim typeel = _sourceTreeResolver.ResolveTypes(sw, typename, True)
  71.         Return Aggregate te As IElement In typeel Where te.ElementType = LanguageElementType.Enum Into FirstOrDefault()
  72.     End Function
  73.  
  74.     Private Sub SwitchEnumExpander_PreparePreview(sender As System.Object, ea As DevExpress.CodeRush.Core.PrepareContentPreviewEventArgs) Handles SwitchEnumExpander.PreparePreview
  75.         ea.AddCodePreview(ea.Element.View.GetSelectionRange.Start, _codeModServices.GenerateCode(CreateSwitchWithEnumCases(ea.Element)))
  76.     End Sub
  77.  
  78. End Class

Для запуска можно просто нажать F5, запустится новый экземпляр Visual Studio, в котором новая функция будет доступна. А вообще DXCore устанавливает плагины в специальную папку и если они там есть, то при запуске студии они подключаются автоматически, то есть никакая установка(как со стандартными надстройками студии) не требуется.

вторник, 10 апреля 2012 г.

Начал изучать платформу Mozilla. Часть 2.

Потихоньку продолжаю осваивать курс. Поначалу это было захватывающим, но на данном этапе стало немного скучновато. Дело не в том, что курс плох, просто подача материала в нем стала напоминать скорее справочник, а не учебник. Не смотря на практические занятия сами лекции посвящены подробному рассказу о назначении каждого элемента и вариантах его использования. Все-таки для хорошего усвоения материала надо дать общие понятия и дать с ними “поиграться”, а дальше уже развивать идеи, снабжая их дополнительными подробностями, иначе книга или учебный курс превращаются в документацию или справочник, но своим первоначальным целям отвечают не вполне. Приятно, что есть такое глубокое изложение всех принципов, но, к сожалению, способ подачи материала таков, что однократного прочтения для освоения всех этих аспектов будет точно недостаточно. Видимо придется потом либо перечитать все заново, “пощупав” технологию своими руками, либо использовать этот курс в дальнейшем именно как справочник. К сожалению как справочник он видимо тоже не лишен недостатков, в силу того, что для справочника не достаточно хорошо систематизирован. Вот такое у меня сейчас впечатление. Возможно потому, что я пока не почувствовал, что усвоил пройденный материал достаточно хорошо.

Проблема с хром-адресацией (описанная в прошлой заметке) решилась довольно быстро. Действительно сейчас все адреса должны быть прописаны в манифестах, но проблема была не в этом. Дело в том, что все, что адресуется через схему chrome, должно находиться в папке chrome приложения (что в принципе и не удивительно), а я почему-то локали и скины разместил в верхнем каталоге приложения, вот и не работало.

Немного знакомился с вопросом о том, какой использовать инструмент для написания кода на javascript, да и не только для того же XUL тоже надо что-то придумывать. Мои прежние попытки найти что-то серьезное для написания кода на JavaScript не привели к тому, чтобы я нашел какой-то инструмент, который бы меня заинтересовал, сейчас я пошел ни куда-нибудь, а на форум Мозиллы, там есть вот такая тема, которая в очередной раз меня убедила в том, что сколько-нибудь качественного инструмента даже для JavaScript не существует, я уже не говорю о раработке для Mozilla.

Из того, что было мной найдено для разработки под Mozilla было несколько расширений для Firefox, где авторы пытались реализовать что-то вроде визуального дизайнера XUl, но выглядит это настолько жалко, что я недолго держал это у себя. Перечислять не буду: не помню да и ищется оно в репозитории расширений Файрфокса по запросу к примеру “XUL”, довольно легко. В общем и целом останавливаться на этом не стоит. Был еще проект xulmaker, в котором авторы что-то пытались реализовать, проект умер довольно давно, да и в его стабильности есть сомнения. Что было полезного в этом проекте, так это схема для XUL, которую они использовали. Для валидации документа схемы не существует, в силу того, что формат допускает появление любых элементов и атрибутов практически в любом месте, а этого в схеме не опишешь, поскольку схема – это набор ограничений. А вот прописать в схеме предусмотренные форматом теги и правила их использования вполне можно.

Как это часто бывает, при попытке использовать схему в Visual Studio вылезла куча ошибок и схема была недоступна из-за них. Можно, конечно, заподозрить студию в том, что с ней что-то не так и она не правильно понимает правильную схему, но на сайте Мозиллы эти ребята предлагают просмотр схемы в браузере с помощью XSLT. Открыв ссылку в Firefox, я получил сообщение об ошибке теперь уже в XSLT. Так что больше склонен думать, что ошиблись разработчики зулмейкера. Схему взялся отредактировать, некоторые ошибки в ней точно были, например множественные декларации. Отредактировал как смог, схема стала доступна и можно попытаться ее использовать. Разместил здесь.

Что касается редакторов JavaScript, то тут тоже пока не определился, что лучше использовать. Редактор Visual Studio не идеален, но все-таки у него есть положительные черты. Он не поддерживает кодфолдинга, подсветки имен идентификаторов(когда при наведении каретки на имя оно подсвечивается везде в коде), да и парные элементы тоже не подсвечиваются. но зато есть надстройки для студии, которые решают эти проблемы(хотя редактор при этом порой начинает тормозить). Рефакторинга кода для этого языка там вообще нет, мое поверхностное знакомство с редактором JavaScript из NetBeans в этом смысле порадовало гораздо больше, когда я, к примеру, захотел переименовать функцию в HTML-документе, то редактор предложил мне сделать это не только в тексте скрипта, но и в атрибутах событий HTML-элементов, что меня даже немного удивило.

Есть правда и несколько приятных вещей, в редакторе студии, которые в принципе позволяют на JavaScript писать не только небольшие сценарии для веб-страниц, но и создавать довольно увесистые проекты. В первую очередь речь конечно же об XML-документировании. Казалось бы такая мелочь не может играть серьезной роли, ан нет – еще как может.

Одной из важнейших проблем этого языка при использовании его в более-менее больших проектах, а не в сценариях веб-страницы, является его слабая типизация. Редактор кода обычно просто не знает какого типа объект, хранящийся в той или иной переменной. Из-за этого он не может сообщить об ошибке, которая может возникнуть из-за несоответствия типов и не может дать подсказку по типам, в силу того, что ему неизвестно по какому именно типу давать подсказку. Кроме того, в сколько-нибудь крупном проекте держать в памяти имена всех функций и типов, а так же сигнатуры функций и состав типов – невозможно в принципе. Поэтому постоянно приходится искать в коде то, что уже написано. Кроме того, даже если разработчик и знает о том, какая функция ему нужна, нет никакой гарантии, что  он не ошибется в ее названии, хотя бы даже в регистре какого-нибудь символа, а редактор кода эту ошибку проглотит. Как это ни странно, но всего 4 XML-элемента для документирования кода и поддержки IntelliSence решают все эти проблемы. В то же время, если сравнивать возможности Visual Studio по редактированию JavaScript, ее нельзя назвать серьезным инструментом. В NetBeans  и Eclipse все описанные возможности так же присутствуют за счет jsdoc, куда можно разместить более обширные данные, многострочные, содержащие ссылки и т. д. Вот здесь обзоры возможностей Netbeans и Eclipse. Естественно даже сравнивать не стоит. Кроме того, в том же Эклипсе можно настроить DOM-объекты, а если учесть, что придется работать с XUL-объектами, у которых свой набор членов, то тут, я думаю, это важно. Таким образом студия не тянет.

И в студии и в эклипсе есть ограничения, например как документировать поля объекта я либо не понял либо это невозможно. С акцессорами свойств проблема есть и там и там(Netbeans  пока не смотрел), то есть они вообще не поддерживаются.

Есть еще одно решение вопроса, это специальные инструменты, транслирующие код с других языков на яваскрипт. Говоря о .Net языках-источниках, наверно самым известным таким проектом является Script#. Однако мне он не очень понравился. Во-перых, там поддерживается только C# в качестве языка-источника. Во-вторых, он реализован как транслятор с C#, причем далеко не все возможности языка поддерживаются. В-третьих, он сильно ориентирован на ASP.Net и Javascript Framework от Microsoft. То, что он генерирует не хочет работать на стороне клиента само по себе, а требует этого самого фреймворка, да еще и файлов сгенерированных сервером. При желании можно все приспособить, но оно того точно не стоит.

А вот что меня на самом деле порадовало, так это jsc . Транслирует он не исходный код, а уже скомпилированную сборку, то есть ему практически безразлично, на каком языке был написан исходник(хотя в документации пишется только о поддержке C#, VB.Net и F#). Код работает на стороне клиента и может распространяться в таком виде, хотя дополнительные библиотеки, сгенерированные jsc тоже нужны. Проекты создаются самые обычные, просто после компиляции кода, запускается из командной строки исполняемый файл проекта(запуск прописан в свойствах проекта после компиляции, так что не обязательно делать это вручную). Такой подход дает возможность использовать все инструменты студии, включая средства отладки кода, всевозможные инструменты типа диаграммы классов и т. д. На бесплатную версию есть ограничение – 30 компиляций в день. Но в принципе, если не запускать программу после каждого построения проекта(например для отладки) то никаких проблем это вызвать не должно.- 30 компиляций вполне хватит.

Буду разбираться дальше, с инструментарием все еще не определился окончательно.

вторник, 3 апреля 2012 г.

Начал изучать платформу Mozilla.

Ну собственно “начал изучать” – слишком громко сказано, просто в данный момент читаю вот этот курс на Интуите и тут просто хочу изложить первые впечатления о лекции и платформе.

Программирование под эту платформу для меня - в первую очередь разработка расширений для браузера Firefox и, возможно, для других приложений, которыми я (пока:)))) не пользуюсь. Но, в то же время, она позволяет так же, используя те же технологии и методы разработки, писать кроссплатформенные standalone-приложения.

Теперь немного о том, что сразу же вызывает затруднения и, судя по всему, и в дальнейшем будет не намного проще. Упомянутый курс на Интуите по всей видимости написан довольно давно и с тех пор кое-что изменилось. В процессе чтения первой лекции у меня было желание послать все к чертям собачьим из-за того, что я нигде в Интернете не мог найти место, где можно скачать платформу Mozilla, для запуска автономных приложений. Справедливости ради следует сказать, что автор курса упомянул о том, что все написанное верно для “классического” браузера, даже версию указал и все такое, тем не менее перспектива изучать технологию, чтобы потом выяснить, что изученное уже давно не актуально и переучиваться меня не особенно вдохновляла. Поэтому я сразу взялся все делать под современную версию платформы. Так вот все мои усилия, направленные на поиск платформы таки увенчались успехом в конце концов. Тот проект, о котором писалось в курсе либо почил с миром(а нынешний – просто его форк), либо был произведен ребрендинг – не суть важно, но в настоящее время среда для запуска автономных приложений на этой платформе называется XulRunner и именно под ним работает то, о чем написано в курсе, но…

Насколько я могу судить, этот курс является наиболее подробным и фундаментальным пособием по вопросу. Из него (да и из собственного опыта) я узнал, что документация по разработке под Мозиллу составлена мягко говоря не самым лучшим образом. Отдельные топики на официальном сайте никак не систематизированы и в одну кучу свалено все, что актуально для разных версий и приходится долго рыться для того, чтобы понять, что подходит для текущей версии, а что уже устарело и если устарело, то найти более новую информацию – задача не такая уж и простая.

В данный момент мне просто выносит мозг chrome-адресация. Идея собственной внутренней адресации весьма недурна. Взять к примеру локализацию приложений все ресурсы локализации находятся в отдельном каталоге, в этом каталоге для каждого языка есть собственный подкаталог, chrome-адресация устроена таким образом, что для всех этих подкаталогов будет использоваться один и тот же адрес, только выбираться конкретная локализация будет в зависимости от настроек системы. Аналогичная ситуация с темами, скинами и прочим. Все это позволяет в тех местах приложения, которые зависят от конкретной локализации или темы указывать один адрес и больше ни о чем не думать. Проблема в том, что все такие адреса надо регистрировать. В курсе говорится, что для этого надо вносить изменения в файл install-chrome.txt, и создавать файл content.rdf. Я довольно долго искал, где находится файл install-chrome.txt, и в лекции(думая, что что-то пропустил) и в сети и в каталогах Файрфокса и Зулраннера, но все было тщетно. Поиск по сети тоже ничего не дал. Потом, изрядно помучившись, до меня начало доходить, что эти данные надо вносить в манифест приложения. Изучив документацию, я попробовал все, но пока ничего не получилось. Поиски информации по этому вопросу в сети тоже пока не принесли результата. Полагаю, в дальнейшем я найду ответ и на этот и на другие вопросы, но все эти поиски настолько утомляют, что довольно велика вероятность того, что все это надоест раньше, чем будет достигнут какой-то результат.

В целом, что порадовало, это то, что для освоения базовых возможностей платформы серьезных усилий не потребуется, если знаком (хотя бы на элементарном уровне) с клиентскими технологиями веб-разработки, такими как: HTML, XML, CSS и JavaScript. Основная масса того, что понадобится для разработки под Мозиллу вполне вписывается в эти технологии.

Весь пользовательский интерфейс описывается на языке XUL, являющемся словарем XML. Есть правда и здесь своя ложка дегтя: дело в том, что сколько-нибудь развитых инструментов для работы с XUL я не нашел, а использовать обычный XML – редактор видимо будет затруднительно, в силу того, что XUL не вписывается ни в какую схему. У него есть собственный набор элементов и атрибутов, но, практически в любом месте документа, можно добавлять собственные элементы. То есть описать документ в схеме вряд ли возможно, а стало быть codecomplition существующих XML редакторов может оказаться полезным только в том случае, если для них сознательно ограничить возможности языка и сразу заложить либо фиксированный набор элементов, либо ввести самостоятельно ограничивающие правила для их появления в документе. Вроде есть в сети такие схемы, но я их еще пробовал использовать.

Способ отображения элементов зависит о стилей CSS, что тоже довольно приятно, в силу того, что не придется изучать что-то новое. О том, какая версия CSS поддерживается в настоящее время говорить трудно, поскольку курс малость устарел и как дела обстоят сейчас я не знаю, но по-моему неплохо. Поддерживаются не все возможности CSS, но в то же время есть масса собственных свойств и значений для стилей. Многие из них достаточно интересны, но как обстоят дела с документацией этих возможностей я не в курсе. Искал документацию по стилям Мозиллы, вводя в поле запроса Гугла специфические для Мозиллы названия свойств, но тот ничего не находил. Видимо придется довольствоваться тем, что есть в курсе.

Логика приложения полностью описывается на JavaScript. Опять-таки проблем это не вызывает. Возможности языка можно расширить компонентами XPCOM, что позволяет выполнить практически все, что может понадобиться. Работать с этим компонентами непросто(по крайней мере новичку), но есть всякие вспомогательные JavaScript-библиотеки, которые облегчают эту задачу(хотя можно обойтись и без них). Примером такой библиотеки может служить jsLib.

Для вводимых пользователем новых элементов XUL существует стиль и поведение по умолчанию, но можно определить и свои собственные. Как минимум это CSS-описание, но так же можно создать документ в формате XBL, который позволяет создавать логику поведения элементов. Я пока еще не дошел до подробного изучения этой темы, но, насколько я понял, это что-то вроде HTML-компонентов (HTC) от Майкрософт. Там эту технологию до ума не довели и забросили, а здесь это работает. Сам формат XBL основан на XML, что тоже приятно.

Данные описываются в RDF/XML, что опять-таки радует. Строки локализации и не только хранятся в DTD-файлах в виде объектов подстановки (ENTITY). Для изменяемых строк(к примеру настроек приложения) тоже есть довольно простой формат файла(что-то вроде ini-файлов, но с расширением .properties).

XulRunner существует для разных платформ и приложения, написанные под него будут работать везде, что, конечно же, не может не радовать. А вот запускать приложения не очень удобно. Фактически никакого exe-файла нет. Есть сам XulRunner и запускать надо именно его, а в качестве аргумента ему следует передавать адрес файла application.ini приложения. Естественно запускать программу каждый раз таким образом не очень удобно, поэтому я вышел из положения следующим образом: создаю переменную среды с именем xulrunner и значением в виде адреса его исполняемого файла xulrunner.exe в кавычках. Теперь, для создания ярлыка мне надо создать его для файла application.ini после чего отредактировать свойства ярлыка так, чтобы адрес выглядел следующим образом
%xulrunner% “c:/appdir/application.ini”
Ну и дальше все как обычно.

Еще из неудобств заметил, что иногда в коде бывают такие ошибки, в результате которых при закрытии приложения процесс Зулраннера остается висеть, не смотря на то, что все формы закрыты. В этой ситуации при повторном запуске приложения, внесенные в него изменения не будут видны. Приходится пользоваться диспетчером задач, чтобы убить процесс. Что-то подобное раньше наблюдалось с Файрфоксом, но потом вроде пофиксили.

Развертывание автономных приложений – тоже проблема. Дело в том, что для их работы нужен XulRunner, таким образом, для того, чтобы приложение работало на другой машине его там придется установить. С одной стороны ничего сложного в этом нет, особенно если учесть, что установка сводится к распаковке архива в любой каталог. С другой – не многим хочется устанавливать 60 МБ Зулраннера для того, чтобы работало небольшое приложение под ним. Если с установкой Явы и Дотнета у некоторых проблемы(даже не смотря на то, что последней версии системы Windows, в которую Дотнет не входит уже около 10 лет), так что можно говорить о Зулраннере, о котором и не знает почти никто? Приложения типа Файрфокса каким-то образом собираются в отдельные сборки и весят не так много, но рядовым разработчикам видимо это не доступно, ну по крайней мере если говорить об общедоступных инструментах, которые позволили бы из такого приложения создать дистрибутивный пакет.