Об использовании WebKit .NET

Введение


Связка HTML+CSS+JavaScript на сегодняшний день зарекомендовала себя как универсальный способ построения пользовательских интерфейсов. Причем не только в веб приложениях, но также в десктоп и мобильных приложениях. Примерами тому являются metro-приложения в Windows 8, фреймворк PhoneGap для создания мобильных приложений.

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

В данной статье мы рассмотрим использование WebKit .NET в десктоп приложении на C# под Windows.


Требования к встраиваемому браузеру


Выявим основные требования к встраиваемому браузеру.

Прежде всего, из C# мы должны иметь возможность устанавливать HTML документу произвольное содержимое — т. е. наш интерфейс. Далее естественным образом возникает потребность в коммуникации между C# и JavaScript. Необходимо иметь возможность вызывать C# из JavaScript и JavaScript из C#, причем двумя способами — синхронно и асинхронно. Наше десктоп приложение с интерфейсом, реализованным во встраиваемом браузере, во многом похоже на традиционное веб приложение, имеющее клиент-серверную архитектуру. Поэтому наличие асинхронных вызовов C# из JavaScript очень важно. Эти вызовы представляют собой аналог AJAX запросов в традиционном веб приложении.

Веб инспектор и отладчик — лучший друг каждого веб разработчика. А как с этим обстоит дело здесь? Скажем сразу — здесь с этим не все так просто. Нам не удалось найти способ отлаживать JavaSсript, выполняющийся во встраиваемом браузере, но мы нашли возможность исследовать dom и получили JavaScript консоль.

Таким образом, основные требования к WebKit .NET заключались в наличии следующих вещей:
  • возможность устанавливать HTML документу произвольное содержимое
  • вызовы JavaScript из C# синхронно или асинхронно
  • вызовы C# из JavaScript синхронно или асинхронно
  • веб инспектор


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

Установка содержимого HTML документа


Свойство WebKitBrowser.DocumentText позволяет установить HTML документу произвольное содержимое. Наш интерфейс полностью независим от внешних ресурсов, весь JavaScript и CSS мы включаем непосредственно в HTML. Для увеличения производительности все скрипты включаются в минифицированном виде.

Вызовы JavaScript из C#


Для вызовов JavaScript из C# в WebKit .NET имеется метод Document.InvokeScriptMethod со следующей сигнатурой:
public Object InvokeScriptMethod(
    string Method,
    params Object[] args
)

К сожалению, данный метод имеет проблему с передачей параметров в JavaScript функцию — она просто не работает.

Чтобы решить эту проблему нам пришлось разработать свой собственный протокол вызовов JavaScript из C#. Идея заключается в следующем:

  • для передачи параметров используется div с заданным идентификатором
  • C# сериализует имя и массив параметров для JavaScript функции в JSON строку и помещает ее в заданный div
  • JavaScript извлекает JSON, десериализует его и вызывает указанную функцию


Код вызова JavaScript функции в C# выглядит следующим образом:
public object CallJS(string functionName, object[] arguments, bool async)
{
    var dict = new Dictionary<string, object>();
    dict["arguments"] = arguments;
    dict["functionName"] = functionName;
    dict["async"] = async;
    
    SetJsBuffer(dict);

    return webKitBrowser.Document.InvokeScriptMethod("JS_CALL_HANDLER");
}

private void SetJsBuffer(object data)
{
    string id = "cs-js-buffer";

    Element elem = null;
    try
    {
        elem = webKitBrowser.Document.GetElementById(id);
    }
    catch (Exception) { } // получим исключение, если элемент не найден

    if (elem == null)
    {
        elem = webKitBrowser.Document.CreateElement("div");
        elem.SetAttribute("id", id);
        webKitBrowser.Document.GetElementsByTagName("body")[0].AppendChild(elem);
    }

    elem.TextContent = Newtonsoft.Json.JsonConvert.SerializeObject(data);
}


А в JavaScript вызов обрабатывается так:
JS_CALL_HANDLER = function() {
    // достаем и десериализуем данные из заданного div
    var dataFromCSharp = getDataFromDOM("cs-js-buffer");

    if (!dataFromCSharp) {
        return;
    }

    if (!dataFromCSharp.async) {
        return window[dataFromCSharp.functionName].apply(
            window,
            dataFromCSharp.arguments
        );
    } else {
        Ext.Function.defer(function () {
            window[dataFromCSharp.functionName].apply(
                window,
                dataFromCSharp.arguments
            );
        }, 1);
    }
}


Чтобы сделать вызов асинхронным мы просто вызываем указанную функцию с
задержкой на 1мс. Так как мы не нуждаемся в асинхронных вызовах, возвращающих значение,
то такое решение нас вполне удовлетворяет.

Вызовы C# из JavaScript


Штатного механизма вызовов С# из JavaScript в WebKit .NET в принципе нет. После пристального вглядывания в документацию было найдено событие WebKitBrowser.DocumentTitleChanged. Это, пожалуй, единственное событие, которое JavaScript легко может генерировать в любой момент путем установки document.title.

Есть две вещи, которые делают это событие привлекательным:
  • document.title можно установить достаточно большим, более 16Кб
  • установка document.title в JavaScript завершается только после выполнения всех обработчиков события DocumentTitleChanged в C#


Это легло в основу нашего протокола вызовов C# из JavaScript.
В следующем листинге приведен JavaScript код, предназначенный для вызовов C#.

var callMap = {}; // для хранения callback функций

/**
 * If call is synchronous, function returns response, received from c#,
 * otherwise - response data will be passed to callback.
 *
 * @param {Object}      config              Call properties
 * @param {String}      config.name         Name of C# function to call
 * @param {Object[]}    config.arguments    Array of arguments, that will be passed to C# function
 * @param {Boolean}     config.async        True, if this request must be performed asynchronously, 
 *                                              in this case callback and scope must be specified
 * @param {Function}    config.callback     Callback function
 * @param {Object}      config.scope        Scope for callback
 */
callCSharp = function(config) {
    var id = generateId(); // уникальный идентификатор вызова

    var callObject = {
        id          : id,
        name        : config.name,
        arguments   : config.arguments,
        async       : config.async
    };

    if (config.async) {
        callObject.callbackHandler = "COMMON_ASYNC_HANDLER";

        callMap[id] = {
            callback: config.callback,
            scope   : config.scope
        };
    }

    // invoke C# by triggering WebKitBrowser.DocumentTitleChanged event
    document.title = Ext.encode(callObject); // elegant, isn't it!
    // execution continues only after all C# handlers will finish

    if (!config.async) {
        var csharpResponse = getDataFromDOM(id);
        return csharpResponse.response;
    }
}


Как видно из листинга, каждый вызов снабжается уникальным идентификатором.
Далее он используется в качестве идентификатора элемента div, в который C# помещает результат.

Если вызов синхронный, то непосредственно перед своим завершением C# обработчик может
поместить свой результат в тело HTML документа. В этом случае метод callCSharp может
извлечь результат из dom сразу после установки document.title.

Для асинхронных вызовов C# должен инициировать запуск callback функций по завершении
обработчиков. В этом случае из C# мы вызываем специальный JavaScript метод, приведенный в следующем листинге:

/**
 * Handler for all C# async requests.
 * It calls real callback according to passed id.
 *
 * C# provides JSON of special format in div with passed id:
 * {
 *     response : {...},
 *     success  : true or false
 * }
 *
 * response and success are passed to callback function
 * @param {String} id Id of C# call
 */
COMMON_ASYNC_HANDLER = function(id) {
    var dataFromCSharp = getDataFromDOM(id);
    var callbackParams = callMap[id];

    delete callMap[id];

    callbackParams.callback.apply(callbackParams.scope, [
        dataFromCSharp.response,
        dataFromCSharp.success
    ]);
}


На стороне C# у нас есть класс CallManager, управляющий подпиской на вызовы из JavaScript. CallManager имеет единственный обработчик события WebKitBrowser.DocumentTitleChanged, который десериализует значение свойства WebKitBrowser.DocumentTitle и, в зависимости от указанного в JavaScript имени (config.name), вызывает соответствующий зарегистрированный обработчик с переданными параметрами. Также CallManager учитывает тип вызова: синхронный или асинхронный. В зависимости от типа обработчик вызывается либо синхронно, либо асинхронно.

Веб инспектор


Мы производим разработку интерфейса в два этапа. На первом этапе мы разрабатываем его как традиционное веб приложение, используя браузер и веб-сервер. Только по достижении желаемого результата мы переходим ко второму этапу — упаковке скриптов и интеграции с C#.

На первом этапе мы активно использовали инструменты разработчика в браузере и имели полный контроль над dom и JavaScript. Но после перехода ко второму этапу весь интерфейс попросту превращался в черный ящик. Возникающие проблемы с версткой и JavaScript становилось достаточно трудно обнаруживать. Мы вынуждены были искать хоть какой-то аналог веб инспектора.

К сожалению, WebKit .NET не позволяет использовать родной веб инспектор WebKit и никакой remote debugging тут не поддерживается. Поэтому мы решили воспользоваться Firebug Lite (1.4.0) — встраиваемым отладчиком, основанным на Firebug.

В HTML страницу Firebug Lite мы подключаем следующим образом:
<!DOCTYPE html>
<html>
<head>
    ...

    <script type='text/javascript' src='/path/to/firebug-lite.js'>
    {
        overrideConsole: true,
        startInNewWindow: true,
        startOpened: true,
        enableTrace: true
    }
    </script>

    ...
</head>
<body>...</body>
</html>


При работе со встраиваемым браузером удобнее, когда Firebug Lite открывается именно в новом окне, что задается опцией startInNewWindow. Но для того, чтобы это произошло необходимы некоторые манипуляции в C#:
browser.Url = new Uri(
    "http://localhost/path/to/debug-version-of-interface.html",
    System.UriKind.Absolute);
    
browser.NewWindowCreated += new NewWindowCreatedEventHandler(
(sender, newWindowEventArgs) =>
{
    // create new form with single item - Firebug Lite window
    debuggerForm = new DebuggerForm(newWindowEventArgs.WebKitBrowser);
    debuggerForm.Show();
});


Конечно, Firebug Lite не поддерживает отладку скриптов, но дает возможность исследовать dom и дает нам JavaScript консоль, а это уже облегчает разработку.

Заключение


После всех описанных выше доработок WebKit .NET превратился во вполне пригодный встраиваемый браузер, который стабильно работает и справляется с достаточно большим dom.

Конечно, реализация интерфейса таким образом сопряжена с определенными сложностями, которые по большей части вызваны отсутствием полноценного веб инспектора, но есть и плюсы. Например, можно повторно использовать JavaScript код из других частей приложения или вообще реализовать одинаковый интерфейс в мобильном, десктоп и веб приложении. Поэтому усилия можно считать оправданными.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 13

    +2
    По поводу вызова C# из JavaScript. Обычно используется перехват перехода по ссылке с использованием своего метапротокола. В коде пишутся вещи подобные такой.

    document.location = "mycoolprotocol:execute?function=sin&x=1.23"

    Все ссылки с нормальынми протоколами вроде http обрабатываются как положено, а такие парсятся и выполняются как нравится.
      0
      Я так делал в QtWebKit. Плюс возможность доступа к классам и методам из JavaScript. Действительно — очень мощная штука.

      Да, и в QtWebKit есть полноценный вебкитовский DevTools.
      0
      В Webkit.NET вебкит-инспектор судя по всему просто отключили, хотя папка с ресурсами инспектора все еще есть в дистрибутиве.
      Еще есть подозрения, что Webkit.NET уже не разрабатывается: последняя версия вышла 30 июня 2011 г. Используется старый рендерер, не поддерживающий аппаратное ускорение и WebGL. Например, вы можете наблюдать там вот такой баг: bugs.webkit.org/show_bug.cgi?id=84245
      К минусам можно отнести тот факт, что Webkit.NET основан на сборке Cairo Windows Port, разработкой которого занимается всего один человек, если я не ошибаюсь — github.com/bfulgham/
        0
        Согласен, такие минусы у WebKit .NET есть, но в целом мы им довольны. Уже почти год его используем. Выбрали его, так как не нашли ничего принципиально лучшего.
          +1
          Awesomnium на мой взгляд лучше
        +1
        А какой профит от использования такой «неочевидной» связки вебкита с .NET? Хотелось бы узнать какие плюсы по сравнению с Winforms и WPF?
          0
          WPF нет под Mono, а WinForms приложения выглядят под Mono достаточно страшно, так что если возникнет потребность портировать приложение под Mono, то, возможно, будет проще. Но основной плюс, на мой взгляд, заключается в том, что можно иметь идентичный интерфейс на десктопе, в мобильном приложение и вебе. Это стало основной причиной использования такой «неочевидной» связки. Если этого не требуется, то конечно проще разработать приложение на WinForms или WPF.
            0
            Webkit уже давно отрисовывает графику с аппаратным ускорением, что, бесспорно, является плюсом в интерфейсостроении.
            К тому же между Silverlight и HTML я выберу HTML из-за CSS

            И попробуйте на WinForms наклепать больше 40-50 контролов на форму или фоновый рисунок для окна сделать (без костылей и нетривиальных твиков). Тормоза? То-то и оно
              0
              Вебкит вебкиту рознь. С чего вы взяли, что Webkit.NET поддерживает аппаратное ускорение?
                0
                Ни с чего. Случайно подумал на Chromium. Беру свои слова назад
            0
            Я вообще много хаков всяких повидал и этот вот

            «Чтобы решить эту проблему нам пришлось разработать свой собственный протокол вызовов JavaScript из C#. Идея заключается в следующем:
            1. для передачи параметров используется div с заданным идентификатором
            2. C# сериализует имя и массив параметров для JavaScript функции в JSON строку и помещает ее в заданный div
            3. JavaScript извлекает JSON, десериализует его и вызывает указанную функцию»

            точно займет достойное место в коллекции.

            Предполагается что это все еще и работает с удовлетворительной скоростью, да?
              0
              Да, работает хорошо. Тем более у нас обычно небольшой JSON передается, так что его сериализация/десериализация не создает никаких проблем.

          Only users with full accounts can post comments. Log in, please.