📜 ⬆️ ⬇️

About using WebKit .NET

Introduction


A bunch of HTML + CSS + JavaScript today has established itself as a universal way to build user interfaces. And not only in web applications, but also in desktop and mobile applications. Examples of this are metro-applications in Windows 8, the PhoneGap framework for creating mobile applications.

Interface implementation using HTML, CSS, and JavaScript primarily means that the interface will be displayed in a browser. If we are considering a desktop or mobile application, then, obviously, the browser must be embedded.

In this article we will look at using WebKit .NET in a desktop C # application for Windows.
')

Embedded Browser Requirements


Identify the basic requirements for embedded browser.

First of all, from C #, we should be able to set up an arbitrary content for the HTML document — that is, our interface. Next, there naturally arises the need for communication between C # and JavaScript. You must be able to call C # from JavaScript and JavaScript from C #, and in two ways - synchronously and asynchronously. Our desktop application with an interface implemented in an embedded browser is in many ways similar to a traditional web application that has a client-server architecture. Therefore, the presence of asynchronous C # calls from JavaScript is very important. These calls are analogous to AJAX requests in a traditional web application.

Web Inspector and Debugger is the best friend of every web developer. And what about this here? Let's say right away - here it is not so simple. We were unable to find a way to debug a javaScript running in an embedded browser, but we found an opportunity to explore the dom and got a javascript console.

Thus, the basic requirements for WebKit .NET were the presence of the following things:


Next, we consider what of the above was present in WebKit .NET and how the missing features were implemented.

Setting HTML Document Contents


The WebKitBrowser.DocumentText property allows you to set arbitrary content for an HTML document. Our interface is completely independent of external resources, we include all JavaScript and CSS directly in HTML. To increase performance, all scripts are included in the minifitsirovanny form.

JavaScript calls from C #


For JavaScript calls from C # in WebKit .NET there is a Document.InvokeScriptMethod method with the following signature:
public Object InvokeScriptMethod( string Method, params Object[] args ) 

Unfortunately, this method has a problem with passing parameters to the JavaScript function - it just does not work.

To solve this problem, we had to develop our own JavaScript call protocol from C #. The idea is this:



The code for invoking a JavaScript function in C # is as follows:
 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); } 


And in JavaScript, the call is handled like this:
 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); } } 


To make the call asynchronous, we simply call the specified function with
a delay of 1ms. Since we do not need asynchronous calls that return a value,
then such a solution fully satisfies us.

C # Calls from JavaScript


There is no regular C # call mechanism from JavaScript in WebKit .NET. After a close look at the documentation, the WebKitBrowser.DocumentTitleChanged event was found. This is probably the only event that JavaScript can easily generate at any time by setting document.title.

There are two things that make this event attractive:


This formed the basis of our C # call protocol from JavaScript.
The following listing shows JavaScript code intended for C # calls.

 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; } } 


As you can see from the listing, each call is provided with a unique identifier.
It is then used as the identifier of the div element in which C # places the result.

If the call is synchronous, just before its completion, the C # handler can
put your result in the body of the HTML document. In this case, the callCSharp method can
extract the result from the dom immediately after setting document.title.

For asynchronous calls, C # should initiate the start of callback functions upon completion.
handlers. In this case, from C # we call the special JavaScript method shown in the following listing:

 /** * 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 ]); } 


On the C # side, we have a CallManager class that controls the subscription to calls from JavaScript. CallManager has a single handler for the WebKitBrowser.DocumentTitleChanged event, which deserializes the value of the WebKitBrowser.DocumentTitle property and, depending on the name specified in JavaScript (config.name), calls the corresponding registered handler with the passed parameters. CallManager also takes into account the type of call: synchronous or asynchronous. Depending on the type, the handler is called either synchronously or asynchronously.

Web Inspector


We develop the interface in two stages. At the first stage, we develop it as a traditional web application using a browser and a web server. Only after achieving the desired result, we proceed to the second stage - packaging scripts and integration with C #.

At the first stage, we actively used developer tools in the browser and had full control over dom and JavaScript. But after the transition to the second stage, the entire interface simply turned into a black box. Problems with layout and JavaScript became difficult to detect. We were forced to look for at least some kind of web inspector.

Unfortunately, WebKit .NET does not allow the use of the native WebKit web inspector and no remote debugging is supported here. Therefore, we decided to use Firebug Lite (1.4.0), an embedded debugger based on Firebug .

In the HTML page of Firebug Lite we connect as follows:
 <!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> 


When working with the embedded browser, it is more convenient when Firebug Lite opens in a new window, which is set by the startInNewWindow option. But in order for this to happen, some manipulations in C # are needed:
 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(); }); 


Of course, Firebug Lite does not support debugging of scripts, but it gives the opportunity to explore the dom and gives us a JavaScript console, and this already facilitates development.

Conclusion


After all the improvements described above, WebKit .NET has become a completely usable embedded browser that works stably and copes with a fairly large dom.

Of course, the implementation of the interface in this way is associated with certain difficulties, which are mostly caused by the absence of a full-fledged web inspector, but there are also advantages. For example, you can reuse JavaScript code from other parts of the application, or even implement the same interface in a mobile, desktop and web application. Therefore, efforts can be considered justified.

Source: https://habr.com/ru/post/154917/


All Articles