Pull to refresh

Первые шаги с Chromium Embedded Framework и .NET

Reading time 5 min
Views 84K
Chromium Embedded Framework (CEF) — это проект с открытыми исходными кодами, созданный в 2008 году как элемент управления Web browser, работающий на базе Chromium от Google.
На данный момент это довольно мощный инструмент для разработки настольных приложений, со списком решений, использующих этот контрол можно ознакомиться здесь. Но достаточно сказать, что его используют такие широко известные продукты, как Evernote и Steam.

Итак, что же дает этот фреймворк?

  • CEF позволяет создать свои обработчики протоколов, таким образом, реализовать свой «закрытый» алгоритм шифрования (да-
    да, несчастные пользователи старого Internet Explorer и корпоративных web-решений, долой ActiveX). Этим же можно воспользоваться, чтобы подгружать данные из статических ресурсов программы
  • CEF позволяет делать обертку над нативными функциями в пространстве объектов виртуальной машины Javascript. Ресурсоемкие операции по обработке больших массивов данных можно переложить на более строгие и быстрые языки программирования
  • CEF позволяет обрабатывать события навигации, скачивания файлов и так далее

В общем, все, что позволит Вам сделать свой собственный браузер наподобие google chrome (только зачем?). Но нам он понадобится, для того, чтобы создавать собственные приложения с HTML5/CSS3 интерфейсом и аппаратно ускоренной графикой.

А теперь о грустном


Библиотека chromiumembedded, ссылка на которую давалась в начале статьи, реализована на C++. Но что делать, если Ваше решение уже работает на другом, управляемом языке программирования? Специально для нас существуют обертки для Java, Delphi, Python и .NET. Об использовании библиотеки CefSharp для .NET и пойдет речь.

Знакомьтесь, CefSharp

CefSharp — библиотека-обертка для chromiumembedded. Однако функционал ее несколько уступает по возможностям последней.
Что же нам доступно:
  1. Создание неограниченного числа компонентов класса WebView
  2. Обработка событий по загрузке страницы, события навигации
  3. Собственные обработчики протоколов.
  4. Внедрение js-кода во время выполнения страницы
  5. Создание глобальных [native code] объектов со статическими методами

Чего не хватает:
  1. Нормальной модели событий. Нет, серьезно, не выглядит это как библиотека .NET:
            public Window(string Url, CefSharp.BrowserSettings settings = null)
            {
                // ...
                _Browser = new WebView(Url, settings ?? new CefSharp.BrowserSettings { DefaultEncoding = "UTF-8" });
                _Browser.PropertyChanged += _Browser_PropertyChanged;
                // ...
            }
    
            void _Browser_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
            {
                if (e.PropertyName == "IsBrowserInitialized" && !isInitialized)
                    doSomeStuff();
                //и так далее
            }
    


  2. Внедрения различных объектов для разных экземпляров класса WebView. Придется смириться с тем, что любой можно вызвать абсолютно любой метод из зарегистрированных Вами
  3. Преобразования типов из CLR в JS и обратно. О костыле для визуального решения этой проблемы речь пойдет дальше
  4. Нельзя связать вызываемый метод с конкретной формой, где расположен WebView.
  5. Не совсем минус, но среда для компилирования CefSharp должна быть Visual Studio 2008.

В качестве поощрения можно отметить то, что начать работать с библиотекой можно довольно быстро (при условии, что у вас есть компилированный вариант), а также тот факт, что планка понимания HTML+JS решений ниже, чем у WPF, и для тех, кто боится уходить в многомесячное изучение сложной технологии, можно просто использовать всеми любимый HTML.

Первое знакомство


Три главные вещи, которые нужны для работы — это локальный обработчик протокола, глобальный объект и тот объект, который будет у нас управлять фреймворком.

Локальный обработчик протокола

Реализуется пара классов: фабрика (реализует интерфейс CefSharp.ISchemeHandlerFactory) и, собственно, сам обработчик (реализующий интерфейс CefSharp.ISchemeHandler).
С первым все понятно:
    public class LocalSchemeHandlerFactory : ISchemeHandlerFactory
    {
        public ISchemeHandler Create()
        {
            return new LocalSchemeHandler();
        }
    }

Второй не будет сложнее:
    public class LocalSchemeHandler : ISchemeHandler
    {
        // здесь могут быть свои конструкторы, личные методы класса и все остальное, что нужно для работы обработчика
        public bool ProcessRequest(IRequest request, ref string mimeType, ref Stream stream)
        {
             // через IRequest мы можем получить ссылку 
             // в mimeType нужно правильно указать MIME-тип данных. Это необходимо, хотя Chromium сумеет и отличить text/javascript от text/plain или image/png
             // в Stream мы передаем поток, зачастую это - MemoryStream данных, что мы считали с жесткого диска или из локального хранилища
             return true; // в случае успешного получения данных. Иначе false
        }
    }

Для того, чтобы подключить js-файл приложения, можно воспользоваться методом GetStream или GetString класса ResourceManager. Из плюсов — исходный код вашего приложения будет находиться внутри .exe или .dll файла. Из минусов — при изменении js-кода придется каждый раз заново компилировать приложение.

Объект-мост между .NET и JS

С ним еще проще — это обычный объект, содержащий методы и поля. Один минус — на весь проект у Вас будет по одному экземпляру каждого такого класса.

Инициализация CEF

Я решил сделать класс наследником ApplicationContext. Для оконного отображения WinForms запускается быстрее, и нет необходимости тянуть за собой WPF
    public class ApplicationController : ApplicationContext
    {
        protected Dictionary<string, object> registeredObjects;

        public ApplicationController(CefSharp.Settings settings = null)
        {
            registeredObjects = new Dictionary<string, object>();
            string resources = Path.Combine(Directory.GetCurrentDirectory(), "cache");
            if (Directory.Exists(resources))
                Directory.CreateDirectory(resources);
            CefSharp.CEF.Initialize(settings ?? new CefSharp.Settings() { 
                Locale = "ru",
                CachePath = resources
            });
            CefSharp.CEF.RegisterScheme("local", new LocalSchemeHandlerFactory());
            registerJsObject("Form", new WindowObject()); // а здесь уже регистрируется объект-мост.
        }

        public void registerJsObject(string Name, object Object)
        {
            if (!registeredObjects.ContainsKey(Name))
            {
                registeredObjects.Add(Name, Object);
                CefSharp.CEF.RegisterJsObject(Name, Object);
            }
        }
    }


На этом, собственно и все. Можно создавать форму, добавлять в нее компонент WebView и работать как душе угодно.
Если Вы дочитали до этого места, то Вы — терпеливый человек и я благодарен Вам.

Но нам этого мало


Как я уже отмечал ранее, в CefSharp есть некоторые недостатки. Например, нельзя связать компонент WebView с формой, его содержащую. Для этого родился некоторого рода жестокий костыль, который я представлю на обозрение публики.
Дабы не захламлять статью лоскутами кода, я приведу некоторые выдержки из листинга.

1 Новый класс Window, наследуемый от Form

  • Он содержит подкласс FormEnumerator, который присваивает каждому окну свой уникальный строковый идентификатор и хранит ссылки на все объекты Window. По методу getWindowById можно получить форму.
  • Статический метод JSInvoke, который получает вызов из среды браузера и вызывает функцию формы
  • Метод CSInvoke, который вызывает метод среды JS из .NET
  • Закрытый метод getFormRefrection, который создает «оболочку» для CLR методов формы. Строка формируется StringBuilder'ом на основании данных рефлексии. Выглядит это примерно так:


2 Объект-мост общих вызовов

Он выполняет операцию по вызову из JS в C#:
public class WindowObject
    {
        public string Invoke(string Id, string Method, string JSONData)
        {
            object x = JSON.JsonDecode(JSONData);
            if (x is ArrayList)
            {
                ArrayList y = (ArrayList)x;
                object[] args = new object[y.Count];
                for (var i = 0; i < args.Length; i++)
                    args[i] = y[i];
                return JScripter.CreateString(Window.JSInvoke(Id, Method, args));
            } else
                return JScripter.CreateString(Window.JSInvoke(Id, Method, new object[] { x }));
        }

        public void Close(string Id)
        {
            Window.FormEnumerator.getWindow(Id).Close();
        }
    }

Внимательный читатель заметит неладное: вместо нормальных объектов CefSharp позволяет обмениваться только простыми типами, такими как int, double, bool, string. Но в реальной жизни обычно, как раз, наоборот. Поэтому данный костыль использует упаковку/распаковку данных в JSON. Решение неидеальное, затрачивается множество времени зря, но таковы данные ограничения библиотеки.
Поскольку DataContractJsonSerializer работает только с определенными типами, его использовать проблематично. Поэтому в проекте был использован 100% managed парсер. Тоже костыль.

С кодом можете ознакомиться здесь.
Tags:
Hubs:
+34
Comments 18
Comments Comments 18

Articles