CEF, Angular 2 использование событий классов .Net Core

Это продолжение статьи CEF, ES6, Angular 2, TypeScript использование классов .Net Core для расширения возможностей.

Как и ожидалось, она не привлекла особого внимания. Но огромное спасибо тем, кого заинтересовало моё творчество. Именно вы даете мне стимул на продолжение изысканий.

Хочу немного остановиться на CEF.

Это кроссплатформенный браузер (с ядром используемым Google Chrome), с неограаниченными расширениями за счет использования натива на С++, позволяющее писать полноценное крооссплатформенное декстопное приложение с UI.

Сегодня я покажу как использовать события объектов .Net Core классов в Angular 2.
Многие прочитав мою первую статью приводили довод, что вместо использования классов .Net можно использовать HTTP сервисы.

Но вот с событиями мы можем делать полноценное декстопное приложение используя торговое оборудование, обмен данными по различным протоколам, использование мессенджеров итд итп.

Для примера возьмем класс с событиями.

 public class EventTest
    {
        public event Action EventWithTwoParameter;
        public event Action EventWithOneParameter;
        public event Action EventWithOutParameter;
        public bool IsRun = false;
        public void Test()
        {
            EventWithTwoParameter?.Invoke(DateTime.Now.ToString(), 1);
            EventWithOneParameter?.Invoke(DateTime.UtcNow.ToString());
            EventWithOutParameter?.Invoke();
        }

        public async void Run()
        {
            if (IsRun) return;
            IsRun = true;

            while (IsRun)
            {
                await Task.Delay(2000);
                Test();
            }
        }
    }

Теперь мы можем использовать этот класс в Angular 2:
export class TestEventComponent {
    EventsRes: EventRes[] = [];
    WOWE: WrapperObjectWithEvents;
   test: any;
    EventTest: any;
    constructor(private ngZone: NgZone) {
        let Net = NetObject.NetWrapper;
      // Получим тип используемого класса.
        this.EventTest = Net.GetType("TestDllForCoreClr.EventTest", "TestDllForCoreClr");
    // Создадим объект
        this.test = new this.EventTest();
// Создадим обёртку для событий, через которую будем подписываться
// и отписываться от событий.
        this.CreateWrapperForEvents(this.test);
    }

// Этот код автоматически создается для уменьшения писанины
// Описывается структура параметров.


    // параметр value:Анонимный Тип
    // Свойства параметра
    // arg1:System.String
    // arg2:System.Int32

    public EventWithTwoParameter(value: any) {
        this.AddComment("EventWithTwoParameter", NetObject.NetWrapper.toString(value));
        value(NetObject.FlagDeleteObject);
    }
    // параметр value:System.String
    public EventWithOneParameter(value: any) {
        this.AddComment("EventWithOneParameter ",NetObject.NetWrapper.toString(value));
    }

    public EventWithOutParameter(value: any) {
        this.AddComment("EventWithOutParameter", NetObject.NetWrapper.toString(value));
    }

    CreateWrapperForEvents(obj: any): void {
        let wrapForEvents = NetObject.GetWrapperForObjectWithEvents(obj, this.ngZone);

        wrapForEvents.AddEventHandler("EventWithTwoParameter", this.EventWithTwoParameter.bind(this));
        wrapForEvents.AddEventHandler("EventWithOneParameter", this.EventWithOneParameter.bind(this));
        wrapForEvents.AddEventHandler("EventWithOutParameter", this.EventWithOutParameter.bind(this));

        // установить переменную wrapForEvents переменной класса
        this.WOWE = wrapForEvents;
    }

Ну и не забыть очистить ссылки на стороне .Net при разрушении компонента:
ngOnDestroy() {  
                NetObject.DeleteNetObjets(this.EventTest, this.test);
                this.WOWE.Close();
                alert("Количество ссылок на стороне .Net ="+Net.CountItemsInStore());
    }

Отписаться от событий можно тремя способами.

// получим результат мотода subscribe объета Subject.

this.AddEventHandlerResult=  wrapForEvents.AddEventHandler("EventWithTwoParameter", this.EventWithTwoParameter.bind(this));

И используя его опишемся от события:
this.AddEventHandlerResult.unsubscribe();

Но события из .Net будут обрабатываться на стороне JS.

Следующие два варианта говорят сам за себя.

this.WOWE.RemoveEventHandler("EventWithTwoParameter");
this.WOWE.RemoveAllEventHandler();

Получить текст TS модуля для описания событий можно получить так:
let DescribeMethodsTS= Net.GetType("NetObjectToNative.DescribeMethodsTS", "NetObjectToNative");
 this.CodeModule = DescribeMethodsTS.GetCodeModuleTS(this.EventTest);

Для чего нужен NgZone можно почитать здесь. Что такое Зоны (Zones)?

Теперь перейдем к подноготной. Для получении обертки событий используется динамическая компиляция. Процесс подробно описан 1С,.Net Core. Динамическая компиляция класса обертки для получения событий .Net объекта в 1С 

Для CEF внесены некоторые изменения:

Код динамической обертки событий
//Данный класс используется для подписки на событие и передачи данных на сторону CEF

 public class ClassForEventCEF
    {
        EventInfo EI;
        public  string EventKey;
        public IntPtr CppHandler;
        public object WrapperForEvent;
        public ClassForEventCEF(object WrapperForEvent, string EventKey, EventInfo EI, IntPtr CppHandler)
        {
            this.EventKey = EventKey;
            this.EI = EI;
            this.CppHandler = CppHandler;
            this.WrapperForEvent = WrapperForEvent;
           // Подпишемся на событие
            EI.AddEventHandler(WrapperForEvent, new System.Action(CallEvent));
        }

        public void CallEvent(object value)
        {
            IntPtr ResIntPtr = AutoWrap.AllocMem(48);
            var EventKeyPtr = WorkWithVariant.WriteStringInIntPtr(EventKey);
            WorkWithVariant.SetObjectInIntPtr(AutoWrap.WrapObject(value), ResIntPtr);
 // Вызовем объектный метод на стороне CEF
// С передачей Ключа события и параметры события
            AutoWrap.EventCall(CppHandler, EventKeyPtr, ResIntPtr);

        }

        public void RemoveEventHandler()
        {
         
            EI.RemoveEventHandler(WrapperForEvent, new System.Action(CallEvent));

        }

    }

Этот класс сформирован динамически:
public class WrapperForEventTestDllForCoreClr_EventTest
 {
 public IntPtr CppHandler;
 public TestDllForCoreClr.EventTest Target;
 Dictionary EventStoage=new Dictionary();
 public event Action EventWithTwoParameter;
 public event Action EventWithOneParameter;
 public event Action EventWithOutParameter;
 
 public WrapperForEventTestDllForCoreClr_EventTest(IntPtr CppHandler, TestDllForCoreClr.EventTest Target)
 {
 
 this.CppHandler = CppHandler;
 this.Target = Target;
 
 Target.EventWithTwoParameter += (arg1,arg2) =>
 {
 if (EventWithTwoParameter!=null)
 {
 var EventWithTwoParameterObject = new {arg1=arg1,arg2=arg2};
 EventWithTwoParameter(EventWithTwoParameterObject);
 }
 };
 
 Target.EventWithOneParameter += (obj) =>
 {
 if (EventWithOneParameter!=null)
 EventWithOneParameter(obj);
 
 
 };
 Target.EventWithOutParameter += () =>
 {
 if (EventWithOutParameter!=null)
 EventWithOutParameter(null);
 };
 
 
 }
 
 public void AddEventHandler(string EventKey, string EventName)
 {
 EventInfo ei = GetType().GetEvent(EventName);
 var forEvent = new ClassForEventCEF(this,EventKey, ei,CppHandler);
 EventStoage.Add(EventKey, forEvent);
 
 }
 
 public void RemoveEventHandler(string EventKey)
 {
 ClassForEventCEF cfe = null;
 if (EventStoage.TryGetValue(EventKey,out cfe))
 {
 EventStoage.Remove(EventKey);
 cfe.RemoveEventHandler();
 
 }
 
 }
 public void RemoveAllEventHandler()
 {
 
 foreach( var cfe in EventStoage.Values)
 cfe.RemoveEventHandler();
 
 EventStoage.Clear();
 }
 
 
 
 public static object CreateObject(IntPtr Self, TestDllForCoreClr.EventTest Target)
 {
 
 return new WrapperForEventTestDllForCoreClr_EventTest(Self, Target);
 }
 }
 
 return new Func(WrapperForEventTestDllForCoreClr_EventTest.CreateObject);


Ну и на стороне JS событие обрабатывается так:
Код обертки событий на стороне TS
class EventEmitter{

    public subject = new Subject();

    constructor(private ngZone: NgZone) {
     //   this.data = Observable.create((observer: any) => this.dataObserver = >observer);

    }

    public subscribe(EventHandler: (value: any) => void) {
        
        return this.subject.subscribe({
            next: (v) => this.ngZone.run(()=> EventHandler(v))
        });
       
        
    }

    public emit(value: any) {
        this.subject.next(value);
       

    }

    public Complete() {
        this.subject.complete();

    }
}

class EventItem {
    constructor(public EventKey: string, public Event:EventEmitter){}
}

//EventEmitter

export class WrapperObjectWithEvents {
    // словарь имен события и EventKey с EventEmitter
    EventsList = new Map();

    // Словарь EventKey и EventEmitter
    EventEmittersList = new Map();
    constructor(private NetTarget: any, private ngZone: NgZone) { };


    // Вызывается при получении внешнего события из .Net
    public RaiseEvent(EventKey: string, value: any) {
        // Если есть подписчики, то вызываем их
        if (this.EventEmittersList.has(EventKey)) {
            let Event = this.EventEmittersList.get(EventKey);
            Event.emit(value);

        }

    }


    public AddEventHandler(EventName: string, EventHandler: (value: any) => void): any {

        let ei: EventItem;
        let isFirst = false;

        if (!this.EventsList.has(EventName)) {
            let EventKey = window.CallNetMethod(0, "GetUniqueString");

            let Event = new EventEmitter(this.ngZone);
            ei = new EventItem(EventKey, Event);
            this.EventsList.set(EventName, ei);
            this.EventEmittersList.set(EventKey, Event);
            NetObject.EventCallers.set(EventKey, this.RaiseEvent.bind(this));
            isFirst = true;
        }
        else
            ei = this.EventsList.get(EventName);


        //  let res = ei.Event.subscribe(this.ngZone.run(() =>EventHandler));
        let res = ei.Event.subscribe((value: any) => { EventHandler(value) });


        if (isFirst)
            this.NetTarget.AddEventHandler(ei.EventKey, EventName);

        return res;


    }

    public RemoveEventHandler(EventName: string) {

        if (this.EventsList.has(EventName)) {
            let ei = this.EventsList.get(EventName);
            let EventKey = ei.EventKey
            this.NetTarget.RemoveEventHandler(EventKey);
            NetObject.EventCallers.delete(EventKey);
            this.EventEmittersList.delete(EventKey);
            this.EventsList.delete(EventName);
            ei.Event.Complete();

        }
    }
    public RemoveAllEventHandler() {
        this.NetTarget.RemoveAllEventHandler();

        for (let ei of this.EventsList.values()) {
            {
                NetObject.EventCallers.delete(ei.EventKey);
                ei.Event.Complete();
            }

            this.EventsList.clear();
            this.EventEmittersList.clear();
        }
    }

    public Close()
    {
        this.RemoveAllEventHandler();
        this.NetTarget(NetObject.FlagDeleteObject);


    }
}


Не сложно сделать передачу JS объектов и функций на сторону .Net только на время вызова метода.

Или по аналогии с Net сделать хранилище объектов JS. Благо в .Net есть финализаторы и не так критично следить за освобождением ссылок.

Хотя разработку всего скачало 5 человек. Но на самом деле там ногого интереного как для программирующих на TS, C# и связки между C++ и .Net.

Если вдруг кого то заинтересовало, то проекты и исходники можно скачать здесь.

Краткое описание содержимого. В каталоге cefsimple\Release\ лежит исполняемый файл с библиотеками и начальной страницей Test.html. В каталоге cefsimple\NetObjectToNative\
лежат все файлы для обмена между CEF и .Net Core. ManagedDomainLoader и ClrLoader отвечают за загрузку .Net Core, получения и передачу методов для обмена данными.

В CefV8HandlersForNet реализованы Хэндлеры для обмена между JS и CEF. В NetConverter конвертация данными между Net и Cef.

В NetObjectToCEF лежат файлы которые реализуют обмен с CEF. В TestDllForCoreClr лежат все используемые примеры для Тестовый.

В файле TestTypeScript\TestTypeScript\app\ лежат файлы ts которые и реализуют Proxy. NetProxy.ts файл реализующий Proxy.

home.component.ts тест с AngleSharp. counter.component.ts различные тесты возможностей. TestSpeed.ts тесты скорости выполнения.

Так же проект без node_modules. установите через вызов в директории TestTypeScript npm install.

Суть тестов такова. Запускаете TestTypeScript и CefProgects\cefsimple\Release\cefsimple.exe. На начальной странице можно попробовать тесты на JS. Для использования тестов на TS нужно перейти на сайт который нужно указать в поле ниже «Введите адрес сайта » что бы перейти на него». Там три теста.

Если хотите компилировать cefsimple. То скачайте отсюда 32-разрядный Standard Distribution и замените в директории tests\cefsimple\ сс и h файлы и скопируйте директорию NetObjectToNative.

Для использования VS 2015 введите в корневом каталоге CEF cmake.exe -G «Visual Studio 14».

Для VS 2017 cmake.exe -G «Visual Studio 15 2017».

Комментарии (7)

  • 9 февраля 2017 в 12:05

    +1

    Такой вопрос, сколько памяти в такой конфигурации кушает Hello World? На все процессы в сумме, если их несколько.

    • 9 февраля 2017 в 12:13 (комментарий был изменён)

      0

      Чесно не смотрел. Но CEF то кушает не мало.
      Если мы хотим создавать кроссплатформенное декстопное приложение, то память не ресурс.
      На 1С я смотрел то там не много. До 10MB. Сама coreclr.dll занимает менее 4 МБ
      Ну и на динамическую компиляцию классов.
      • 9 февраля 2017 в 12:25 (комментарий был изменён)

        +1

        Если мы хотим создавать кроссплатформенное декстопное приложение, то память не ресурс.

        Ну вот из-за этого у появляются «нативные приложения на JS», кушающие по 400 мегабайт после запуска (Skype for Linux Alpha, я смотрю на тебя). В авалонии у нас каталог контролов на старте выглядит вот так:


        Скрытый текст
        93d19dd0d5ee784539f5d7b53689ea95.png

        Если прокликать все вкладки слева, то прогрузятся картинки и кушать начнёт 33МБ. Это на полноценный XAML-фреймворк с биндингами и прочим. И мы считаем, что это очень много и надо ужать потребление на таком простом приложении хотя бы мегабайт до 20. .NET Core, кстати, ощутимо лучше в этом плане себя ведёт чем десктопный дотнет и Mono.

        Мы сами CEF рассматривали в качестве реализации WebView, но он очень уж прожорлив и, видимо, придётся использовать webkit напрямую.

        • 9 февраля 2017 в 12:36 (комментарий был изменён)

          0

          Значит так посмотрел.
          CEF с начальной страницей без Angular занимает 20 mb
          С вызовом простеньких методов доходит до 30mb
          Если подключить динамическую компиляцию то вырастает до 70 mb

          Если подключить Angular 2 То размер сразу достигает 90 МБ.
          Но вот дальше даже использую динамическую компиляцию размер не переходит 100 МБ

        • 9 февраля 2017 в 12:37

          0

          Кстати скачай посмотри. А то всего то 5 человек скачали. Сам и посмотришь. Может, что и посоветуешь
          • 9 февраля 2017 в 12:52 (комментарий был изменён)

            +1

            Так это счастье на гитхаб бы выложить, в 2017-ом году-то. Там, глядишь, и народ подтянется.


            Посоветовать наврятли что смогу, я во всей этой истории с «а давайте напишем гуй на Angular (тогда ещё первый), запихнём в CEF (у нас это был CEFGlue) и заставим взаимодействовать с хостовым приложением (виндоформы)» разочаровался, когда оно начало кушать до гигабайта памяти, а на машинах целевой аудитории просто не могло работать.


            А так тема с нормальными вызовами дотнета из любой фигни очень интересная, да. Было бы неплохо к postgres прикрутить поддержку хранимок на шарпе, например, по аналогии с PL/Java, например.

            • 9 февраля 2017 в 12:59

              0

              Вот я с C++ не очень дружу. Они кстати тоже через файлы распространяют.
              Ну Angular 2 то развивается и WebPack тоже. Сейчас с ним разберусь.
              У меня есть статья про 1С, Linux, Excel, Word, OpenXML, ADO и Net Core

              Сейчас NetStandard 2 и .Net Core 1.2 выйдут и возможностей будет близки к взрослому .Net

© Habrahabr.ru