⬆️ ⬇️

Cross-platform GUI on C # and web technologies

The very first product specification, partly oral, contained a requirement - the presence of a cross-platform (Windows, Linux, Mac) client for the desktop and a lightweight version of the mobile (Windows, Android, iPhone). If possible, the interface should be as similar as possible to different operating systems.

Thanks to Mono, we can write cross-platform applications, but the issue with the GUI remains open. The existing technologies under .Net (Windows Forms, WPF) work well only under Windows, and we already had the sad experience of porting Windows Forms. Under Linux, we can use GtkSharp , but I don’t like the idea of installing Mono on Windows with .Net. As a result, you have to write and maintain a separate interface for each OS.

What could a .Net command (with a web bias) come up with in this situation? We decided to embed Webkit and write GUI on a bunch of html-js-css.

Today, we have been successfully using this approach for Windows for 2 years and a year for Linux and Mac. Until the mobile platform until the hands did not reach.





What for?



Identical interface under all platforms. Only minor differences are possible when drawing fonts, when displaying elements. The latter is always due to errors in the layout.

Development under one OS. Empirically, we have found that it is enough to conduct the main development for Windows, and under the other platforms only to check sometimes. For example, before release.

All the power of web development. This is especially true if the team consists of web developers. You can use html5, css3, familiar approaches and libraries. We, by the way, use a popular framework for building web applications, as a result we have an interface only on js.

Separation into frontend and backend. There is an opportunity to conduct a separate development of the presentation and application logic, coordinating the API. For example, our interface is a full-fledged web application that interacts with the “server” through ajax requests. In the desktop application we emulate the processing of these requests. Thus, you can develop and debug the interface using the Chrome developer tools, throwing the necessary mock answers to the local server. Especially self-confident developers who have enough access to the dom and console can use firebug lite in the desktop application.

There is something to write on Habr. Such experiments add excitement when developing and brighten up the harsh everyday life of a programmer.



How?



For each platform, we create a native application whose GUI consists of one user interface element — a browser stretched across the entire window.

We need to learn how to display html in the browser, find a way to make calls to js-C # and C # -js. Differences in calls may seem strange, but there is a simple explanation - in the browsers used, different functions are implemented and work.

')

Mac OSX


There is no choice what to embed under poppy. Therefore, we use MonoMac and a standard browser. But there is a catch in the licenses. You can freely distribute the application without Mono, i.e. the user himself will have to put Mono and, therefore, the application can not get into the AppStore. If we want to embed Mono in the application, then we will have to buy Xamarin.Mac , which will cost $ 300 or $ 1000 depending on the size of the company for one programmer.

Under the poppy turned out the most concise code. The only non-intuitive place is the C # call from js.

After initializing the browser, we need to create an object through which js can call controller methods from C #. Let's call the interaction object:

webView.WindowScriptObject.SetValueForKey(this, new NSString("interaction")); 


We define the methods and indicate which of them can be called from js:

  [Export("callFromJs")] public void CallFromJs(NSString message) { CallJs("showMessage", message + "   C#"); } [Export ("isSelectorExcludedFromWebScript:")] public static bool IsSelectorExcludedFromScript(MonoMac.ObjCRuntime.Selector sel) { if (sel.Name == "callFromJs") return false; return true; //      } 


Now in js we can call the callFromJs method:

  window.interaction.callFromJs('  js.'); 


Full listing of the declared functionality with comments
  public partial class MainWindowController : MonoMac.AppKit.NSWindowController { /*  */ //  xib(nib) ,      UI   public override void AwakeFromNib () { base.AwakeFromNib (); //     js    C#.   interaction // window.interaction.callFromJs(param1, param2, param3) -    js. webView.WindowScriptObject.SetValueForKey(this, new NSString("interaction")); webView.MainFrame.LoadHtmlString (@" <html> <head></head> <body id=body> <h1></h1> <button id=btn> C#</button> <p id=msg></p> <script> function buttonClick() { interaction.callFromJs('  js.'); } function showMessage(msg) { document.getElementById('msg').innerHTML = msg; } document.getElementById('btn').onclick = buttonClick; </script> </body> </html>", null); } //    ,       js [Export ("isSelectorExcludedFromWebScript:")] public static bool IsSelectorExcludedFromWebScript(MonoMac.ObjCRuntime.Selector aSelector) { if (aSelector.Name == "callFromJs") return false; return true; //      } [Export("callFromJs")] public void CallFromJs(NSString message) { CallJs("showMessage", new NSObject[] { new NSString(message + "   C#") }); } public void CallJs(string function, NSObject[] arguments) { this.InvokeOnMainThread(() => { webView.WindowScriptObject.CallWebScriptMethod(function, arguments); }); } } 




Working example on github.

I really missed this video when I understood: “How to add a link to a WebView in the controller code”.





Ubuntu


Under Mono, use the webkit-sharp package.

The quantity of not intuitively clear code smoothly increases.

To call C # from js, you can intercept the transition by reference.

  browser.NavigationRequested += (sender, args) => { var url = new Uri(args.Request.Uri); if (url.Scheme != "mp") { //mp - myprotocol. //     . //        return; } var parameters = System.Web.HttpUtility.ParseQueryString(url.Query); handlers[url.Host.ToLower()](parameters); //    browser.StopLoading(); }; 


The call from js will look like this:

  window.location.href = 'mp://callFromJs?msg=  js.'; 


Another way is tied to the TitleChanged event.

In js, set the title of the document:

  document.title = JSON.stringify({ method: 'callFromJs', arguments: { msg: '  js'} }); 


In C #, the TitleChanged event fires, we deserialize the title and, similarly to the previous approach, call the handler.



In the considered WebKit wrapper, from C # you can execute any js code, which allows us to implement the js call from C #:

  public void CallJs(string function, object[] args) { // javascript var js = string.Format(@" {0}.apply(window, {1}); ", function, new JavaScriptSerializer().Serialize(args)); Gtk.Application.Invoke(delegate { browser.ExecuteScript(js); }); } 


Full listing of the declared functionality with comments
  public partial class MainWindow: Gtk.Window { private Dictionary<string, Action<NameValueCollection>> handlers; private WebView browser; public MainWindow (): base (Gtk.WindowType.Toplevel) { Build (); CreateBrowser (); this.ShowAll (); } protected void OnDeleteEvent (object sender, DeleteEventArgs a) { Application.Quit (); a.RetVal = true; } private void CreateBrowser () { //       js handlers = new Dictionary<string, Action<NameValueCollection>> { { "callfromjs", nv => CallJs("showMessage", new object[] { nv["msg"] + "   #" }) } }; browser = new WebView (); browser.NavigationRequested += (sender, args) => { var url = new Uri(args.Request.Uri); if (url.Scheme != "mp") { //mp - myprotocol. //     . //        return; } var parameters = System.Web.HttpUtility.ParseQueryString(url.Query); handlers[url.Host.ToLower()](parameters); //    browser.StopLoading(); }; browser.LoadHtmlString (@" <html> <head></head> <body id=body> <h1></h1> <button id=btn> C#</button> <p id=msg></p> <script> function buttonClick() { window.location.href = 'mp://callFromJs?msg=  js.'; } function showMessage(msg) { document.getElementById('msg').innerHTML = msg; } document.getElementById('btn').onclick = buttonClick; </script> </body> </html> ", null); this.Add (browser); } public void CallJs(string function, object[] args) { var js = string.Format(@" {0}.apply(window, {1}); ", function, new JavaScriptSerializer().Serialize(args)); Gtk.Application.Invoke(delegate { browser.ExecuteScript(js); }); } } 




Working example on github.



Windows


We develop the main development under Windows.

The details have already been described by my colleague a year ago and during this time nothing has changed in principle. To some extent, this indicates the reliability of the approach. Also, the article contains more details that it is enough to consider using the example of one OS.

I will only add an example on github.



Special features


Such an interesting way of presenting the interface has its own features, which you should know if you decide to repeat our path.

Additional time consumption during construction. Preparation for embedding an interface as a resource into an application takes some time: Saas, pasting files, minification. But when developing the interface in the browser, there is no need to rebuild the interface or the application each time.

The increase in the consumption of RAM. This is the only serious disadvantage of this approach. In our case, the browser consumes 50 megabytes of RAM. On the one hand, this is not much, but if the target audience assumes the old technique, then this feature will have to be taken into account. Although it would be unclear whether a similar interface implemented on another technology will consume less memory. In any case, we have the consumption of RAM by the browser - a black box. No other systemic problems or performance failures were noticed by us.

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



All Articles