Кроссплатформенный GUI на C# и веб-технологии

от автора

Самая первая спецификация продукта, частично устная, содержала требование – наличие кроссплатформенного(Windows, Linux, Mac) клиента под десктоп и облегченную версию мобильного(Windows, Android, iPhone). По возможности интерфейс должен быть максимально похожим на разных ОС.
Благодаря Mono мы можем писать кроссплатформенные приложения, но вопрос с GUI остается открытым. Имеющиеся технологии под .Net(Windows Forms, WPF) хорошо работают только под Windows, и у нас уже был печальный опыт портирования Windows Forms. Под Linux мы можем использовать GtkSharp, но идея ставить Mono на Windows при наличии .Net мне не нравится. В итоге приходится писать и поддерживать отдельный интерфейс под каждую ОС.
Что в этой ситуации могла придумать команда .Net(с уклоном под веб)? Решили встраивать Webkit и писать GUI на связке html-js-css.
На сегодняшний день мы 2 года успешно используем такой подход для Windows и год – под Linux и Mac. До мобильной платформы пока руки не дошли.

Зачем?

Идентичный интерфейс под всеми платформами. Возможны лишь незначительные отличия при отрисовке шрифтов, при отображении элементов. Последнее всегда объясняется ошибками в верстке.
Разработка под одну ОС. Эмпирическим путем нами было выявлено, что достаточно вести основную разработку под Windows, а под остальными платформами лишь иногда проверять. Например, перед релизом.
Вся сила веб-разработки. Особенно это актуально, если команда состоит из веб-разработчиков. Можно использовать html5, css3, привычные подходы и библиотеки. Мы, кстати, используем популярный фреймворк для построения веб-приложений, в итоге у нас интерфейс только на js.
Разделение на frontend и backend. Появляется возможность вести отдельно разработку представления и логики приложения, согласовав апи. Например, наш интерфейс — это полноценное веб-приложение, взаимодействующее с «сервером» через ajax-запросы. В десктоп приложении эмулируем обработку этих запросов. Таким образом, можно разрабатывать и отлаживать интерфейс с использованием инструментов разработчика в Chrome, закинув необходимые mock ответы на локальный сервер. Особо уверенные в себе разработчики, которым достаточно доступа к dom и консоли, могут использовать firebug lite в десктоп приложении.
Есть о чем написать на хабр. Подобные эксперименты добавляют азарта при разработке и скрашивают суровые будни программиста.

Как?

Под каждую платформу создаем нативное приложение, GUI которого состоит из одного элемента пользовательского интерфейса – браузера, растянутого во все окно.
Нам нужно научиться отображать html в браузере, найти способ осуществлять вызовы js-C# и С#-js. Различия в вызовах могут показаться странными, но есть простое объяснение – в используемых браузерах реализован и работает разный функционал.

Mac OSX

Выбора что встраивать под маком нету. Поэтому используем MonoMac и стандартный браузер. Но тут есть подвох в лицензиях. Можно свободно распространять приложение без Mono, т.е. пользователь сам должен будет поставить Mono и, следовательно, приложение не может попасть в AppStore. Если же мы хотим встроить Mono в приложение, то придется покупать Xamarin.Mac, который обойдется в 300 или 1000 долларов в зависимости от размера компании за одного программиста.
Под мак получился самый лаконичный код. Единственное не интуитивно понятное место — вызов С# из js.
После инициализации браузера нам надо создать объект, через который js сможет вызывать методы контроллера из C#. Назовем объект interaction:

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

Определяем методы и указываем, какие из них могут быть вызваны из 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; // Запрещаем вызов всех остальных методов     } 

Теперь в js мы можем вызвать метод сallFromJs:

    window.interaction.callFromJs('Вызов из js.'); 

Полный листинг заявленного функционала с комментариями

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

Рабочий пример на github.
Этого видео мне очень не хватало, когда я разбирался: «Как добавить ссылку на WebView в код контроллера».

Ubuntu

Под Mono используем пакет webkit-sharp.
Плавно увеличивается количество не интуитивно понятного кода.
Для вызова C# из js можно перехватывать переход по ссылке.

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

Вызов из js будет выглядеть так:

    window.location.href = 'mp://callFromJs?msg=Сообщение из js.'; 

Еще один способ завязывается на событие TitleChanged.
В js устанавливаем title у документа:

    document.title = JSON.stringify({         method: 'callFromJs',         arguments: { msg: 'Сообщение из js'}     }); 

В С# срабатывает событие TitleChanged, мы десериализуем title и аналогично предыдущему подходу вызываем обработчик.

В рассматриваемой обертке WebKit можно из С# исполнять любой js код, что позволяет нам реализовать вызов js из 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);         });     } 

Полный листинг заявленного функционала с комментариями

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

Рабочий пример на github.

Windows

Основную разработку мы ведем под Windows.
Подробности уже были описаны моим коллегой год назад и за это время принципиально ничего не изменилось. В какой-то степени это свидетельствует о надежности подхода. Также в статье больше деталей, которые вполне достаточно рассмотреть на примере одной ОС.
Я лишь добавлю пример на github.

Особенности

У такого интересного способа представления интерфейса есть свои особенности, о которых стоит знать, если вы решите повторить наш путь.
Дополнительный расход времени при строительстве. Подготовка для встраивания интерфейса как ресурса в приложение занимает некоторое время: Saas, склеивание файликов, минификация. Но зато при разработке интерфейса в браузере нет необходимости каждый раз перестраивать ни интерфейс, ни само приложение.
Увеличение расхода оперативной памяти. Это единственный серьезный минус данного подхода. Браузер в нашем случае потребляет мегабайт 50 оперативной памяти. С одной стороны это немного, но если целевая аудитория предполагает старую технику, то придется принимать во внимание эту особенность. Хотя будет ли аналогичный интерфейс, реализованный на другой технологии, потреблять меньше памяти – непонятно. В любом случае, у нас расход оперативной памяти браузером – черный ящик. Других системных проблем или проседаний производительности нами замечено не было.

ссылка на оригинал статьи http://habrahabr.ru/company/tiktokcoach/blog/204286/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *