Введение
Связка 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 код из других частей приложения или вообще реализовать одинаковый интерфейс в мобильном, десктоп и веб приложении. Поэтому усилия можно считать оправданными.
Автор: amenshov