На Тему Дня: Кроссплатформенный Клиент Telegram На .Net Core И Avalonia

В этой статье я покажу вам, как реализовать кроссплатформенное приложение с использованием .

NET Core и Avalonia. Тема Telegram в последнее время пользуется большой популярностью — тем интереснее будет создать для нее клиентское приложение.



На тему дня: кроссплатформенный клиент Telegram на .
</p><p>
NET Core и Avalonia

В статье затрагиваются довольно базовые концепции развития Авалонии.

Однако мы не будем писать «Hello, World».

Вместо этого предлагается рассмотреть реальное приложение.

Изучим как общую архитектуру приложения, так и отдельные компоненты.

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

Настоящий код всегда возможен посмотри на GitHub .

Текст статьи носит образовательный характер, но сам проект вполне реальный.

Цель проекта — создание клиента, предназначенного для использования в качестве рабочего инструмента.

Многие идеи были позаимствованы у других мессенджеров и перенесены в модель Telegram. Проект находится в стадии разработки и на данный момент не подходит для повседневного использования.

Данной статьей автор, помимо всего прочего, надеется привлечь к разработке единомышленников.



Введение

Наше приложение будет основано на фреймворке Avalonia. Мы будем активно использовать паттерн MVVM и Rx.NET. XAML используется в качестве языка разметки для создания пользовательского интерфейса.

Telegram будет использоваться для связи с API библиотека TDLib и автоматически генерируется привязки для .

NET .

Реактивное программирование будет широко использоваться в разработке.

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

Если вы знакомы с WPF, вам будет относительно легко перейти на Avalonia. Знакомство с такими вещами, как React.js, тоже не помешает.

На тему дня: кроссплатформенный клиент Telegram на .
</p><p>
NET Core и Avalonia

Avalonia скрывает от разработчика детали реализации конкретной платформы.

Программист обычно имеет дело с компонентами верхнего уровня.

Так, например, для создания нового приложения вам потребуется установить пакеты Avalonia, Avalonia.Desktop и в функции Main написать следующие строки:

  
  
  
  
  
  
  
   

AppBuilder .

Configure(new App()) .

UsePlatformDetect() .

UseReactiveUI() .

Start<MainWindow>(() => context);

Это типичный Builder, знакомый каждому, кто имел дело с .

NET Core и ASP.NET Core. Ключевая строка — UsePlatformDetect. Avalonia заботится об определении среды, в которой работает программа, и настраивает серверную часть для отображения пользовательского интерфейса.

App и MainWindow здесь — классы, унаследованные от Avalonia.Application и Avalonia.Window соответственно, их назначение должно быть примерно понятно из названий, мы к ним вернемся позже.

Если вы используете расширение для VisualStudio , то он предоставит шаблон, содержащий реализацию этих классов.

Давайте воспользуемся расширением и создадим проект. Мы обнаружим, что проект будет содержать следующие файлы:

.

/App.xaml .

/App.xaml.cs .

/MainWindow.xaml .

/MainWindow.xaml.cs

Как видите, это те же самые классы App и MainWindow, упомянутые ранее, дополненные файлами XAML. Каждый из этих классов будет содержать вызов: AvaloniaXamlLoader.Load(this).

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

NET-объекты, «заполняя» целевой объект, переданный в качестве аргумента.

Если вам нужно разобраться в деталях работы XAML, вы можете получить их из других источников — подойдет любая книга по WPF. Для простых случаев в этом нет необходимости; достаточно будет научиться работать с компонентами, которые Avalonia предоставляет «из коробки».

Аналогично реализованы элементы управления (представление) в Avalonia, т.е.

файлы XAML по сути нужны для декларативного описания иерархии, которая затем преобразуется в обычные объекты в памяти приложения.

Пример такой иерархии: кнопка, вложенная в форму, которая, в свою очередь, находится внутри окна.



<Window> <Panel> <Button> <TextBlock>Foo Bar</TextBlock> </Button> </Panel> </Window>

Avalonia содержит предопределенный набор элементов управления, таких как TextBlock, Button и Image. Для их составления в более сложные структуры используются элементы управления-контейнеры: Grid, Panel, ListBox и т. д. Все эти элементы управления работают аналогично тому, как они реализованы в WPF, т. е.

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

Реализация MVVM

Мы попробуем разделить внутреннее состояние приложения и его отображение.

Состояние будет храниться в некоторой иерархии объектов (модель представления).

Представление будет реагировать на изменения в модели представления и перестраивать пользовательский интерфейс.

А Модель Представления, в свою очередь, может меняться под воздействием двух факторов: пользователя или внешних событий.

Нажатие кнопки — это пример пользовательского события из View, но новое сообщение чата — это внешнее событие.

В Авалонии модель представления неразрывно связана с термином «контекст данных» или просто «контекст».

Я буду использовать все понятия взаимозаменяемо.



На тему дня: кроссплатформенный клиент Telegram на .
</p><p>
NET Core и Avalonia

Иерархия модели представления часто аналогична структуре представления, по крайней мере, в первом приближении.

Мы отдадим View полностью под управление Avalonia, т.е.

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

Структура верхнего уровня модели представления выглядит следующим образом (псевдокод):

App { .

Index # int .

Auth { .

Phone # string Password # string } Main { Nav { .

Contacts # ReactiveList<Contact> } Chat { .

Messages # ReactiveList<Message> } } }

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

Корневой DataContext передается Builder при создании объекта MainWindow (см.

выше), и в будущем именно он будет управлять всей иерархией модели представления.

Представление задает контекст для вложенных элементов управления с помощью механизма привязки.

На практике это нужно для установки значений свойств объекта и подписки на их изменения.

Обратите внимание, как привязки используются для задания:

  1. Свойства SelectedIndex элемента управления Carousel (определяет, какую страницу отображает приложение — форму авторизации или чат)
  2. Текстовые свойства для TextBox (связывает значение в модели с текстом формы ввода номера телефона и пароля)
  3. Все вложенные контексты


<Window DataContext="{Binding App}"> <Carousel SelectedIndex="{Binding Index}"> <Panel DataContext="{Binding Auth}"> <TextBox Text="{Binding Phone, Mode=TwoWay}" /> <TextBox Text="{Binding Password, Mode=TwoWay}" /> </Panel> <Grid DataContext="{Binding Main}"> <Panel DataContext="{Binding Nav}"> <ListBox Items="{Binding Contacts}" /> </Panel> <Panel DataContext="{Binding Chat}"> <ListBox Items="{Binding Messages}" /> </Panel> </Grid> </Carousel> </Window>

В этом примере AppContext содержит два дочерних контекста: MainContext и AuthContext. AppContext управляет жизненным циклом вложенных контекстов: он отвечает за их инициализацию и освобождение.

На практике это выглядит так: после запуска приложения AppContext проверяет, авторизован ли пользователь, и если нет, инициализирует дочерний AuthContext. Графический интерфейс приложения реагирует на создание AuthContext, отображая форму авторизации.

Пользователь вводит учетные данные, авторизуется, AppContext подписывается на событие авторизации, он освобождает AuthContext и одновременно инициализирует MainContext. SelectedIndex переключается с 0 на 1, чтобы удалить форму входа и показать чат. MainContext, в свою очередь, содержит еще два контекста: ChatContext и NavigationContext. Контекст навигации будет создан во время инициализации MainContext, потому что в этот момент мы уже знаем, что пользователь авторизован, и у нас есть возможность загружать контакты.

С ChatContext все немного интереснее: его создание (и одновременно освобождение предыдущего контекста) происходит, когда пользователь выбирает чат в меню навигации.

Сам ChatContext будет подписан на внешние события, такие как добавление, редактирование и удаление сообщений.

Дисплей, соответственно, отреагирует рисованием сообщений или их удалением.

При этом контекст должен подписываться на события только выбранного чата, поскольку события другого чата нас не интересуют. Контекст чата также реагирует на пользовательские события, такие как ввод нового сообщения.



На тему дня: кроссплатформенный клиент Telegram на .
</p><p>
NET Core и Avalonia

Вложенные модели обычно не содержат ссылок на родительский контекст, но имеют возможность взаимодействовать с внешними компонентами для получения от них событий или совершения вызовов (например, оболочка над TDLib).



Асинхронность

Как и большинство фреймворков с графическим интерфейсом, Avalonia позволяет выполнять действия с элементами пользовательского интерфейса только из потока пользовательского интерфейса.

Желательно выполнить минимальный объем работы в этом потоке, чтобы приложение оставалось отзывчивым.

С появлением async/await делегировать работу другим потокам стало намного проще.

Подход RX.NET очень похож на async/await, но он также упрощает работу с сериями событий.

Приложение широко использует возможности Observable для обеспечения асинхронности.

Рассмотрим пример — загрузка контактов пользователя.

После загрузки приложения пользователь должен увидеть список своих контактов.

В нашем случае контактом является имя и фотография пользователя.

Сама загрузка — это типичный запрос данных через сеть, т.е.

это действие однозначно лучше выполнять вне UI-потока.

Простым решением было бы использовать async/await: основной поток инициирует загрузку, а когда она завершается, получает уведомление и показывает контакты.

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



На тему дня: кроссплатформенный клиент Telegram на .
</p><p>
NET Core и Avalonia

Казалось бы, с таким подходом проблем нет. Но, присмотревшись, вы увидите, что только 10% времени (приблизительные цифры) приложение выполняло запрос на получение списка контактов, остальные 90% времени были заняты загрузкой и декодированием изображений.

Все это время пользователь ждал.

Есть ли лучший подход? Почему бы нам не показать список контактов сразу после выполнения первого запроса, а изображения загрузить во «второй волне»? Эту задачу в принципе можно решить с помощью TPL, но для такого сценария лучше подходит использование Rx.NET. Идея очень проста: мы точно так же делегируем загрузку данных другому классу, но на этот раз ожидаем в ответ Observable вместо Task. Это позволит нам подписаться на серию событий, а не на одно: первое событие будет представлять собой загруженный список контактов, а каждое последующее событие будет нести какое-то обновление (например, загруженное фото).

Давайте рассмотрим загрузку контактов на примере.

Контекстная задача включает в себя подписку на результат LoadContacts. Обратите внимание на вызов метода ObserveOn — это инструкция Rx.NET выполнить код, переданный в Subscribe в потоке планировщика Avalonia. Без этой инструкции мы не имеем права изменять свойство «Контакты», поскольку код будет выполняться в потоке, отличном от потока пользовательского интерфейса.



// NavContext.cs class NavContext : ReactiveObject { private ReactiveList<Contact> _contacts; public ReactiveList<Contact> Contacts { get => _contacts; set => this.RaiseAndSetIfChanged(ref _contacts, value); } public NavContext(ContactLoader contactLoader) { contactLoader.LoadContacts() .

ObserveOn(AvaloniaScheduler.Instance) .

Subscribe(x => { Contacts = new ReactiveList(x.Contacts); x.Updates .

ObserveOn(AvaloniaScheduler.Instance) .

Subscribe(u => { u.Contact.Avatar = u.Avatar; }); }); } }

ContactLoader отвечает за выполнение сетевого запроса.

После завершения запроса создается еще один Observable, отвечающий за доставку обновлений подписчикам.

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

Обновления будут доставляться по мере их загрузки.



// ContactLoader.cs class ContactLoader { IObservable<Load> LoadContacts() { return Observable.Create(async observer => { var contacts = await GetContactsAsync(); // networking var updates = Observable.Create(async o => { foreach (var contact in contacts) { // load avatar from remote server // .

var avatar = await GetAvatarAsync(); // networking o.OnNext(new Update(avatar)); } o.OnComplete(); }); observer.OnNext(new Load(contacts, updates)); observer.OnComplete(); }) } }

Последовательность событий можно контролировать: объединять, фильтровать, трансформировать и т. д. Это очень удобно, когда имеется большое количество источников событий и самих событий.

Rx.NET позволяет эффективно работать с Observable. Небольшой пример: если закешировать фото на диске, загрузка будет существенно быстрее, однако такое ускорение может привести к проблеме большого количества обновлений за короткий промежуток времени, что усложнит работу планировщика, и может привести к потере отзывчивости приложения.

Чтобы этого избежать, мы будем использовать буферизацию: мы будем обрабатывать все обновления, произошедшие в течение ста миллисекунд сразу, и за один заход отфильтровывать вхождения, не содержащие фотографии (по каким-то причинам).



x.Updates .

Where(u => u.Avatar != null) .

Buffer(TimeSpan.FromMilliseconds(100)) .

ObserveOn(AvaloniaScheduler.Instance) .

Subscribe(list => { foreach (var u in list) { u.Contact.Avatar = u.Avatar; } });



Заключение

Подробно рассказать о каждой используемой технологии в одной статье невозможно.

Я постарался выбрать самое интересное и изложить их в сжатой форме.

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

Для дальнейшего изучения рекомендую посетить следующие ссылки:

На тему дня: кроссплатформенный клиент Telegram на .
</p><p>
NET Core и Avalonia

Теги: #.

NET #.

net core #avalonia #Windows #linux #macOS #скажем нет электроне и наркотикам #.

NET #C++ #разработка MacOS #разработка Linux #разработка Windows

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.