Comments 22
Как и в большинстве GUI фрэймворков, Avalonia позволяет выполнять действия с элементами пользовательского интерфейса только с UI-потока. На этом потоке желательно выполнять минимум работы, чтобы приложение оставалось отзывчивым. С приходом async/await делегировать выполнение работы в другие потоки стало намного проще.

В WPF, насколько я помню, все async/await методы, которые вызывались со стороны UI, выполнялись в тоже UI потоке, если явно не указывали планировщик отличный от текущего контекста синхронизации.

Вот только автор его не использует, посмотрите как метод LoadContacts() написан...

Мне в коде очень не нравится вот это место:


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), потому что вы помните что можно делать а что нельзя. Но стоит в проект прийти новым разработчикам, или вам вернуться через год неактивности — и привет многопоточность.


Выхода из этой ситуации — три.


  1. Сделать Contact потокобезопасным, чтобы можно было установить ему свойство Avatar из любого потока, а не только из потока UI.


  2. Сделать Contact неизменяемым. В таком случае у него не будет свойства Avatar, и понадобится отдельный класс ContactViewModel про который будет знать только контекст.


  3. Сделать Contact неразделяемым между потоками: после передачи в OnNext ContactLoader обязан забыть про существование этого объекта.

Сделать свойства потокобезопасными будет очень накладно: для каждого свойства придется писать какой-то код, который будет выполнять INotifyPropertyChanged на нужном потоке. Производительность тоже надо мерить при таком подходе.


Мутабильные они "by design". Объясню почему. В любой момент с сервера Телеграма может прилететь апдейт — например, пользователь сменил имя или аватарку. Этот апдейт в конечном счете тоже дойдет до контекста, где нужно выполнить смену значения для этого свойства.


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

Все три варианта все еще подходят.

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

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

Еще замечания, не очень критичные.


Код внутри Observable.Create будет выполняться в потоке UI, потому что именно в нем происходит подписка. Однако в текущей версии ему доступ к UI не нужен, так что для ускорения его желательно выгнать в фоновой поток:


return Observable.Create(async observer =>
{
    // ...
}).SubscribeOn(Scheduler.Default);

Также можно убрать все вызовы OnComplete: асинхронная версия Observable.Create сама вставит их когда завершится задача.

Oтличная рабoта, x2bool! UI у EGram замечательный — группирoвки чатoв, каналoв и бoтoв — этo именнo тo, чегo так не хватает в других десктoпных и мoбильных прилoжениях Telegram. Также неверoятнo класснo, чтo такие замечательные фреймвoрки, как AvaloniaUI и ReactiveUI, вышли в массы и начинают активнo испoльзoваться сooбществoм независимых разрабoтчикoв.

Думаю, с м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м же духе! :)

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

Честно говоря, код ContactLoader ужасен. Observable.create надо применять тогда, когда по-другому никак. А тут каша из вложенных Observable.create, тасков и циклов. Await в цикле, кстати, тоже зло. Предлагаю 2 варианта:
В первом используем 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)
        });

А какой иде на маке пользуетесь? VS for Mac с ходу собрать не удалось. Собирается только командой dotnet. И да, при запуске на маке не находит libtdjson.dylib. Как поправить? На винде запускается нормально.
Я собирал и запускал с помощью dotnet build и dotnet Egram.dll. На винде запустилось, на маке — нет.
Ок, попробую, спасибо.
Попробовал. Оказывается, такой файл там уже был, однако ошибка присутствует.

Хм. Даже не знаю. Многие жаловались на Windows, но там проблему вроде решили: https://github.com/x2bool/egram.tel/issues/1. А я сам на маке, и оно у меня точно работает. Если не трудно, заведете issue и стэктрейс запостите туда?

Only those users with full accounts are able to leave comments. Log in, please.