IronPython как движок для макросов в .NET приложениях

    Подозреваю, многие из вас задумывались — как можно в .NET приложение добавить поддержку макросов — чтобы можно было расширять возможности программы без ее перекомпиляции и предоставить сторонним разработчикам возможность легко и просто получить доступ к API вашего приложения? В статье рассмотрено, как в качестве основы для выполнения макросов использовать IronPython — реализацию языка Python на платформе .NET.

    Для начала, следует определится — что мы будем иметь в виду под словом «макрос» — это скрипт, который без перекомпиляции проекта позволял бы получить доступ к определенному API. Т.е. вытаскивать значения с формы, модифицировать их — и все это в режиме run-time, без модификации приложения.

    Первым вариантом, который приходит на ум будет создание собственного интерпретатора для простенького скрипт-языка. Вторым — будет динамическая компиляция какого-нибудь .NET языка (того же C#) — с динамической же подгрузкой сборок и выполнением через Reflection. И третий — использование интерпретируемых .NET языков (DLR) — IronPython или IronRuby.

    Создавать свой язык + интерпретор к нему с возможностью .NET interoperability — задача нетривиальная, оставим ее для энтузиастов.
    Динамическая компиляция — слишком громоздко и тащит за собой использование Reflection. Однако, этот метод не лишен преимуществ — написанный макрос компилируется единожды и в дальшейшем может использоватся многократно — в виде полноценной .NET сборки. Итак — финалист — метод номер три — использование существующих DLR языков. В качестве такого языка выбираем IronPython (примите это как факт :). Текущая версия IPy — 2.0, взять можно на codeplex.com/IronPython

    Перейдем непосредствено к кодированию.
    Для начала, рассмотрим интерфейс тестового приложения «Notepad».

    Image Hosted by ImageShack.us


    В меню «Сервис» и разместим пункт «Макросы». Для примера рассмотрим простейший вариант формирования списка макросов — в каталоге с программой создадим папку «Macroses» файлы из этой папки станут пунктами меню.

    private void Main_Load(object sender, EventArgs e)
        {
          MacrosToolStripMenuItem itm = null;
          string[] files = Directory.GetFiles(@".\Macroses");
          foreach (string file in files)
          {
            itm = new MacrosToolStripMenuItem(Path.GetFileNameWithoutExtension(file)) { MacrosFileName = file };
            itm.Click += new EventHandler(macroToolStripMenuItem_Click);
            макросыToolStripMenuItem.DropDownItems.Add(itm);
          }
        }

    internal class MacrosToolStripMenuItem : ToolStripMenuItem
    {
    public MacrosToolStripMenuItem(string FileName) : base(FileName) { }
    public string MacrosFileName { get; set; }
    }


    * This source code was highlighted with Source Code Highlighter.


    MacrosToolStripMenuItem — класс-наследник от ToolStripMenuItem отличающийся только свойством MacrosFileName

    Для начала, создадим макрос, который просмотрит текст в textBox'е и найдет все e-mail адреса вида «vpupkin@mail.ru». В папке Macroses создаем файл SaveEmail.py, запускаем приложение — и смотрим, что в меню Макросы появился пункт SaveEmail.

    Теперь собственно ключевой момент — выполнение IPy скрипта и доступ его к интерфейсу. Добавляем к проекту ссылку на сборку IronPython.dll. И создаем класс MacroRunner — выполняющий скрипт.

    public class MacroRunner
      {
        public static Form CurrentForm;

        public string FileName { get; set; }

        public MacroRunner() { }

        public void Execute()
        {
          // собственно среда выполнения Python-скрипта
          IronPython.Hosting.PythonEngine pyEngine = new IronPython.Hosting.PythonEngine(); 
          // важный момент - к среде выполнения подключаем текушую выполняемую сборку, т.к.
          // в ней собственно и объявлена форма, к которой необходимо получит доступ
          pyEngine.LoadAssembly(System.Reflection.Assembly.GetExecutingAssembly());      

          try
          {
            pyEngine.ExecuteFile(FileName);
          }
          catch (Exception exc)
          {
            MessageBox.Show(exc.Message);
          }
        }
      }


    * This source code was highlighted with Source Code Highlighter.


    Ключевой момент — подключение к выполняющей среде IPy текущей сборки — для доступа к форме. Когда сборка подключена, в IPy скрипте появится возможность использовать классы пространства имен Notepad. Так же, через LoadAssebmly можно добавить и другие необходимые сборки — типа System.Windows.Forms — чтобы работать с формами.
    Класс готов, теперь модифицируем обработчик клика на пунктах подменю Макросы

    protected void macroToolStripMenuItem_Click(object sender, EventArgs e)
        {
          MacrosToolStripMenuItem item = sender as MacrosToolStripMenuItem;

          MacroRunner runner = new MacroRunner() { FileName = item.MacrosFileName };
          MacroRunner.CurrentForm = this;
          runner.Execute();
        }


    * This source code was highlighted with Source Code Highlighter.


    Здесь следует отметить следующий момент — чтобы передать в IPy-скрипт форму, из которой собственно вызывается макрос — используется статическое поле CurrentForm. В скрипте форма будет доступна как Notepad.MacroRunner.CurrentForm. В идеале, скрипт, разумеется, не должен иметь полного доступа к интерфейсу формы — а должен пользоватся только предоставленным API — и ограничиваться только им. Но сейчас этим заморачиваться не будем — и просто сделаем textBox открытым (Modifier = Public). Ну и кроме текстового поля, разрешим скрипту доступ к пункту меню Сервис (Modifier = Public).

    Работа с формой закончена, собираем проект и открываем файл SaveEmail.py — теперь работаем только с макросами.

    Итак, первый макрос — SaveEmail.py:

    from Notepad import *
    import re

    text = MacroRunner.CurrentForm.textBox.Text
    links = re.findall("\w*@\w*\.\w{2,4}", text)
    file = open("emails.txt", "w")
    file.write("\n".join(links))
    file.close()


    * This source code was highlighted with Source Code Highlighter.


    Т.к. сборка подключена к среде выполнения — то доступно пространство имен Notepad — в котором объявлены классы приложения. Как раз сейчас потребуется статический метод класса MacroRunner — чтобы получить доступ к активной форме (еще раз оговорюсь — что правильнее было бы предоставить не прямой доступ, а через класс-посредник — которые ограничит доступ определенным API). Ну а дальше все просто — получаем текст, регулярным выражением вытаскиваем email — и сохраняем их в файл в текущем каталоге.

    Можно запустить приложение, ввести произвольный текст, содежащий email — и убедиться, что после того, как макрос отработал — в папке с выполняемой программой появился файл emails.txt.

    Теперь еще один пример, что может сделать макрос — чуть интереснее предыдущего. Итак, создаем в папке Macroses файл UIModifier.py. Как можно догадаться по названию — макрос будет изменять элементы интерфейса приложения. Конкретно — добавит новый пункт в меню Сервис. Для того, чтобы можно было работать с элементами управления WinForms необходимо в среде выполнения IPy подключить сборку System.Windows.Forms. Это можно сделать в момент запуска скрипта из приложения — добавить еще один вызов LoadAssembly. Но мы решили — никаких перекомпиляций, пусть IronPython обходится своими силами. Ну что ж, силы есть :). Чтобы подключить сборку используется метод AddReference класса clr.

    from Notepad import *
    main = MacroRunner.CurrentForm

    import clr
    clr.AddReference("System.Windows.Forms")
    from System.Windows.Forms import *

    def pyHello(s,e):
      MessageBox.Show("Hello from IPy!")

    item = ToolStripMenuItem()
    item.Name = "pybtn"
    item.Text = "Python created!"
    item.Click += pyHello

    main.сервисToolStripMenuItem.DropDownItems.Add(item)


    * This source code was highlighted with Source Code Highlighter.


    Все просто — получаем текущую форму, подключаем сборку System.Windows.Forms и импортируем из пространства имен System.Windows.Forms все — пригодится.
    pyHello — простенький обработчик события — при щелчке на созданном пункте меню — будет выводится сообщение.

    Запускаем приложение, выполняем макрос. Смотрим пункт меню Сервис:

    Image Hosted by ImageShack.us


    При щелчке на пункт меню «Python сreated!» появится стандартный MessageBox — собственно, чего и добивались.

    Спасибо всем за внимание :)

    Поделиться публикацией

    Похожие публикации

    Комментарии 38

      0
      Прочел с удовольствием. Статья очень интересная. Но… как быть с безопасностью самой софтины и отловом ошибок?
        +2
        1) Безопасность — надо иметь продуманный API который просто не позволит сделать лишнего :)
        2) А с отловом ошибок в чем проблема? IronPython.Hosting.PythonEngine будет бросать исключения — а там уже можно реагировать как нужно. Сообщать пользователю, вести лог etc.
          0
          Ну вот, это и имел в виду
            +1
            1) Думаю, без явного указания пермишшинов для загружаемых в хост-среду сборок не обойтись. Иначе макросы-«злоумышленники» смогут безнаказанно и к БД обращаться, и диски форматировать, и по сетке данные гонять…
          0
          Автору спасибо. Наглядный пример преимуществ .NET платформы.

          Было бы воообще замечательно если бы еще продемонстрировали пример создания примитивного API хотябы из нескольких команд…
            +1
            Могу добавить — хотя тут ничего нет сложного. Просто вместо прямого обращения к свойству textBox.Text будет использоваться класс-посредник. Будет что то вроде TextProcessor.Text, а textBox на форме будет private.
            А то сейчас можно сделать например так: textBox.Hide() — и поля ввода не будет :) что не есть хорошо.
            0
            Спасибо за интересный материал.

            Интересно будет подумать о таком подходе для создания расширений ASP.NET сайтов. Некие пользовательские скриптики. В принципе всё то же самое, только API надо очень детально прописывать и заботится о безопасности сильнее.
              0
              Все таки не будем путать — для ASP.NET сайтов пользователи будут использовать клиентские технологии — типа JavaScript. Хотя думаю IronPython можно прикрутить через Silverlight контрол :) — но это уже извращение конечно…
                0
                Боюсь вы меня привратно поняли. JS — это исключичтельно на клиенте. А я говорю про сервер. Макросы, которые будут расширять функциональность сайта.

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

                У фейсбука есть похожий механизм — facebook applications

                Так вот я говорю о подобном.
                  0
                  Да, я действительно не так понял :)

                  Можно дополнить статью применением IPy для расширения возможностей сайта :)
                  0
                  Почему IronPython + Silverlight = извращение? о_О
                  www.voidspace.org.uk/ironpython/silverlight/index.shtml
                    0
                    Нет, нет. SL + IPy это красота :)
                    Я просто не так понял комментарий — я думал предлагается создавать клиентские скрипты для ASP.NET сайтов на IPy.
                0
                Подобный подход используется, кстати в Miranda IM и очень успешно. Там в качестве плагина можно установить mBot, подсунуть Миранде php5ts.dll (дллка от PHP) и писать скрипты используя API от Миранды.
                  +1
                  Ничо, нормально.
                  <grammar nazi>
                  Слово «макрос» в единственном числе — «macro».
                  Во множественном числе — «macros». Не «macroses»! :)
                  en.wiktionary.org/wiki/macro
                  </grammar nazi>
                    0
                    а с дебагом и IDE для IronPyton как? можно ли это в Visual Studio писать (что бы синтакс подсвечивался и выпадали автодополнения) и дебажить?
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Ну вот по поводу среды для IPy на основе Visual Studio — у меня не слишком хорошие впечатления остались. Как то странно работает IntelliSense. А особо кроме него и подсветки мне ничего не нужно от среды :)
                            0
                            Сейчас действительно нет хорошей IDE для IronPython. Будем надеятся, что с выходом релизной сборки ситуация изменится.
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                Да я так, мечтаю… :)
                            0
                            спасибо! будет чем завтра на работе заняться :)
                              0
                              Хотя правда в C# 4 уже можно будет и на C# макросы писать :) Он насколько я понял будет нацелен на runtime разработку.<a href = '«platforma2009.ru/materials/showitem.aspx?MID=88e431c5-c36a-4ff0-87d1-0b5ae1cb7e72»> Можно посмотреть здесь
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  0
                                  Если речь о dynamic — это не то. Это шаг к «утиной типизации» — но от необходимости компиляции не избавляет.
                                  0
                                  спасибо! очень круто. понравилось.
                                    0
                                    Для этих целей еще существует проект Python for .NET. Можете ли что нибудь сказать про него? В чем преимущества/недостатки перед IronPython?
                                      0
                                      Python.NET позволяет просто получить из стандартного питона доступ к .NET сборкам. IronPython — это реализация языка Python для .NET, интерпретатор написан на C# и представляет собой обычную сборку, которую можно подключить к своим проектам и использовать.
                                      0
                                      Спасибо! Очень полезно. Недавно задался вопросом, как реализовать скриптовую систему, думал уж, что придется делать через Reflection. Теперь знаю, как лучше)
                                        0
                                        Статья хорошая, но не о ней хочу спросить. Вот у Вас в C# коде есть место, где при создании нового пункта меню с очередным макросом, свойству присваивается значение: { MacrosFileName = file };
                                        Как описание это возможности найти в MSDN? По «new» не вижу ничего похожего.
                                          0
                                          Это возможность C# 3.0, если не ошибаюсь. Суть в том, что можно в фигурных скобках у конструктора задавать значения для свойств создаваемого класса. Например:

                                          Button btn = new Button()
                                          {
                                          Content = «This is a button»,
                                          Width = 100,
                                          Height = 50
                                          };

                                          Так же, можно инициализировать значения в списке, например new List() { 1, 2, 3 };
                                            0
                                            Извините, сплю уже. Не List, а new List() { 1, 2, 3 };.
                                              0
                                              В примере с List — это инициализация массива. Так же можно делать для new int[] {1,2,3}

                                              При создании объекта можно в фигурных скобках инициализировать поля — new Object1() { Field1 = value }. Как совершенно верно было отмечено, это особенность C# 3.0
                                              0
                                              А, это хабр есть угловые скобки)) Короче, здесь List — это список интегеров.
                                                0
                                                Да понял про суть. Я просто хотел бы почитать про это в MSDN, но не могу найти, где такое описано.
                                                  0
                                                  Это спецификация C# 3.0
                                                  MSDN
                                                    0
                                                    Спасибо
                                              0
                                              Спасибо большое за статью! Не удалось ли Вам нормально подключить System.IO?
                                              import clr
                                              clr.AddReference(«System»)
                                              from System.IO import *
                                              так импорт отрабатывает, но всякие StreamReader-ы не доступны
                                              явно укзать from System.IO import Directory — ругается, что не может подключить имя Directory
                                              IPy юзаю версии 2.01, кстати, там хостить слегка по-другому надо, чем у Вы написали.

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое