Comments 22
Как и в большинстве GUI фрэймворков, Avalonia позволяет выполнять действия с элементами пользовательского интерфейса только с UI-потока. На этом потоке желательно выполнять минимум работы, чтобы приложение оставалось отзывчивым. С приходом async/await делегировать выполнение работы в другие потоки стало намного проще.
В WPF, насколько я помню, все async/await методы, которые вызывались со стороны UI, выполнялись в тоже UI потоке, если явно не указывали планировщик отличный от текущего контекста синхронизации.
См. ConfigureAwait и TAP.docx.
Мне в коде очень не нравится вот это место:
u.Contact.Avatar = u.Avatar;
Здесь что u.Contact
, что u.Avatar
— это объект который пришел от к NavContext от ContactLoader. Про них обоих ContactLoader знает. Так с какого перепугу установка свойства Avatar
оказалась в ответственности NavContext?
Зачем вообще ContactLoader возвращает объект который можно обработать одним-единственным способом? Чтобы больше boilerplate писать?..
Надо или присваивание перенести в NavContext, или убирать свойство Avatar из DTO контакта.
Это правда спорное место. ContactLoader мог бы сам выполнить код на планировщике Avalonia. Преследуется несколько целей: 1) работу с UI-потоком выполнять только из классов Context для простоты и однообразности 2) Дать свободу контексту выполнять обновления в бэкграунде, если сущность не привязана к UI в данный момент.
Для того чтобы не ловить проблемы с многопоточностью в случайных местах, любой разделяемый между потоками объект должен быть либо потокобезопасным, либо неизменяемым.
У вас же Contact (или Conversation) не является потокобезопасным, но при этом изменяемый. Да, сейчас у вас все работает (ценой хака с u.Contact.Avatar = u.Avatar
), потому что вы помните что можно делать а что нельзя. Но стоит в проект прийти новым разработчикам, или вам вернуться через год неактивности — и привет многопоточность.
Выхода из этой ситуации — три.
Сделать Contact потокобезопасным, чтобы можно было установить ему свойство Avatar из любого потока, а не только из потока UI.
Сделать Contact неизменяемым. В таком случае у него не будет свойства Avatar, и понадобится отдельный класс ContactViewModel про который будет знать только контекст.
- Сделать Contact неразделяемым между потоками: после передачи в OnNext ContactLoader обязан забыть про существование этого объекта.
Сделать свойства потокобезопасными будет очень накладно: для каждого свойства придется писать какой-то код, который будет выполнять INotifyPropertyChanged на нужном потоке. Производительность тоже надо мерить при таком подходе.
Мутабильные они "by design". Объясню почему. В любой момент с сервера Телеграма может прилететь апдейт — например, пользователь сменил имя или аватарку. Этот апдейт в конечном счете тоже дойдет до контекста, где нужно выполнить смену значения для этого свойства.
Как лучше реализовать все эти кейсы, я не придумал. Если подскажете, буду только рад.
Еще замечания, не очень критичные.
Код внутри Observable.Create
будет выполняться в потоке UI, потому что именно в нем происходит подписка. Однако в текущей версии ему доступ к UI не нужен, так что для ускорения его желательно выгнать в фоновой поток:
return Observable.Create(async observer =>
{
// ...
}).SubscribeOn(Scheduler.Default);
Также можно убрать все вызовы OnComplete
: асинхронная версия Observable.Create
сама вставит их когда завершится задача.
Думаю, с мoей стoрoны будет уместным упoмянуть в кoмментариях библиoтеку PropertyChanged.Fody — с пoмoщью этoгo инструмента мoжнo значительнo упрoстить кoдoвую базу прилoжения, убрав шаблoнные геттеры и сеттеры, и даже нескoлькo увеличить прoизвoдительнoсть oтправки уведoмлений XAML-интерфейсам.
Приведу пример. Вместo этoгo:
public class ContactsViewModel : ReactiveObject
{
private ReactiveList<Contact> _contacts;
public ReactiveList<Contact> Contacts
{
get => _contacts;
private set => this.RaiseAndSetIfChanged(ref _contacts, value);
}
}
С PropertyChanged.Fody будет дoстатoчнo написать следующее (Привет, АOП!):
[AddINotifyPropertyChangedInterface]
public class ContactsViewModel
{
public ReactiveList<Contact> Contacts { get; private set; }
}
А ещё этoт инструмент активнo пoддерживается сooбществoм и недавнo мы егo сдружили с реактивными oбъектами ReactiveUI. С бoлее пoдрoбным сравнением пoдхoдoв к oписанию мoделей представления с пoмoщью ReactiveUI, ReactiveProperty и PropertyChanged.Fody мoжнo oзнакoмиться в этoй заметке. Пример реактивнoй мoдели представления, приправленнoй кoдoгенерацией, мoжнo найти здесь.
Надеюсь, этo смoжет пoмoчь и сделать чью-нибудь жизнь прoще.
Спасибo за ваш труд. Пoжалуйста, прoдoлжайте в тoм же духе! :)
Что-то домен egram.tel не резолвится.
https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet?tabs=netcore2x — всё что нужно там
В первом используем Task в случае, когда нужно асинхронно вернуть одно значение. В Rx.net нет типа Single.
async Task<Load> LoadContacts()
{
var contacts = await GetContactsAsync();
var avatarUpdatesStream = contacts
.ToObservable()
.SelectMany(contact => GetAvatarAsync(contact)
.ToObservable()
.Select(avatar => new Update(contact, avatar))
);
return new Load(contacts, avatarUpdatesStream)
}
Второй вариант — только Rx, без TPL
IObservable<Load> LoadContacts() =>
GetContactsAsync()
.ToObservable()
.Select(contacts =>
{
var avatarUpdatesStream = contacts
.ToObservable()
.SelectMany(contact => GetAvatarAsync(contact)
.ToObservable()
.Select(avatar => new Update(contact, avatar))
)
return new Load(contacts, avatarUpdatesStream)
});
Rider. Возможно, VS for Mac не выполняет инструкцию копирования библиотек в итоговую сборку. Можно попробовать руками скопировать в output: https://github.com/x2bool/egram.tel/blob/master/Egram/libtdjson.dylib
Ок, попробую, спасибо.
Хм. Даже не знаю. Многие жаловались на Windows, но там проблему вроде решили: https://github.com/x2bool/egram.tel/issues/1. А я сам на маке, и оно у меня точно работает. Если не трудно, заведете issue и стэктрейс запостите туда?
На злобу дня: кроссплатформенный клиент для Telegram на .NET Core и Avalonia