Автоматический BusyIndicator для асинхронных операций и не только

    Использование такого компонента как BusyIndicator привнесит в наше приложение приятные (индикация процесса) и полезные (блокировка элемента управления) качества. Однако до последнего времени я редко использовал его, т.к. при асинхронном получении источника данных приходилось постоянно писать дополнительный код для включения/выключения. При синхронной работе ситуация вроде как упрощается, но использование MVVM-модели всё-равно требует дополнительных телодвижений. Особенно, если BusyIndicator добавляется в самом конце разработки формы.

    Поэтому, я решил максимально автоматизировать этот процесс, чтобы мне не приходилось писать ни строчки кода. Итак:

    Постановка задачи:
    1. Обеспечить автоматическую индикацию для любых списков, использующих в качестве источника данных значение свойства ItemsSource.
    2. Признаком, что данные получены будем считать не пустое значение свойства ItemsSource.
    3. В качестве BusyIndicator'а можно использовать любой элемент управления только бы он имел boolean-свойство IsBusy.
    4. Всё дополнительное кодирование должно быть реализовано в представлении (View) формы и XAML-код должен иметь такой шаблон:
      <BusyIndicator ...>
          <ListBox ItemsSource="{Binding DataList, IsAsync=true}" ...>
              ...
          </ListBox>
      <BusyIndicator>
      


    Свойство биндинга IsAsync=true в примере можно опустить, в этом случае рассуждения особо не изменяться, но в данной статье я буду приводить примеры именно асихронного получения данных, т.к. если требуется индикация процесса, то получение данных занимает ощутимое время, а раз так, то мы же не хотим, чтобы наше приложение зависало при этом, а раз так — то асинхронный биндинг наше всё. Тем более, что реализовать его нам ничего не стоит: IsAsync=true в XAML-е и в коде ViewModel'и:

    private IEnumerable _dataList = null;
    public IEnumerable DataList
    {
        get
        {
            if (_dataList == null)
                _dataList = Model.GetDataList(...);
            return _dataList;
        }
        private set
        {
            if (_dataList == value) return;
            _dataList = value;
            NotifyPropertyChanged("DataList");
        }
    }
    
    public void RefreshDataList()
    {
        DataList = null;
    }
    


    Первое, что мне пришло в голову (и это даже визуально заработало) — это написать нечто наподобие такого:
    <BusyIndicator IsBusy="{Binding DataList, IsAsync=true, Converter={StaticResource NullToBool}">
        <ListBox ItemsSource="{Binding DataList, IsAsync=true}" ...>
            ...
        </ListBox>
    <BusyIndicator>
    


    А ну, кто сможет сразу раскритиковать этот код?

    Вот и мне он тоже сразу не понравился. Отладка подтвердила мои опасения: получение списка происходило дважды — для BusyIndicatorа и для списка.

    «Не беда!» — сказал я и чуть изменил метод получения списка:
    private object _dataListSync = new nbject();
    private IEnumerable _dataList = null;
    public IEnumerable DataList
    {
        get
        {
            lock (_dataListSync)
            {
                if (_dataList == null)
                    _dataList = Model.GetDataList(...);
                return _dataList;
            }
        }
    }
    


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

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

    Следующей идеей было использование attached property.

    До сих пор мне не выдавалась возможность глубоко в этом разобраться на реальном интересном примере, так что в следующей статье Создание Attached Property для BusyIndicator шаг за шагом я расскажу, что у меня получилось.

    Спасибо тем кто дочитал до конца.
    Поделиться публикацией

    Похожие публикации

    Комментарии 6
      0
      > Признаком, что данные получены будем считать не пустое значение свойства ItemsSource.
      По мне так скверная идея. Мало ли почему очищается ItemsSource. Если следовать паттерну MVVM, то ничто не мешает во ViewModel завести свойство IsBusy и биндить к нему ваш адорнер.

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

      > При синхронной работе ситуация вроде как упрощается, но использование MVVM-модели всё-равно требует дополнительных телодвижений.
      При переходе на асинк телодвижений дополнительных будет ровно столько, сколько требует обновления самой логики на асинхронную работу. И дело тут не в паттерне MVVM.
        0
        >> > Признаком, что данные получены будем считать не пустое значение свойства ItemsSource.
        >> По мне так скверная идея. Мало ли почему очищается ItemsSource. Если следовать паттерну MVVM, >> то ничто не мешает во ViewModel завести свойство IsBusy и биндить к нему ваш адорнер.

        Не спорю, можно завести свойство IsBusy, но что будет написано в геттере этого свойства? Посмотрите на приведенный код DataListItemsSource никогда не будет равняться null, кроме как в момент получения значения. Зачем же городить огород? В самом начале я сказал, что моей целью есть минимизация кода.
        Разумеется могут встретится случаи когда пустое значение ItemsSource будет означать пустой список, а не промежуточное перед получением данных значение, и в таких редких случаях, естественно нужно делать так, как предложили вы. Но в 99% случаев можно упростить, я считаю.

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

        Тут я не понял, что вы имели в виду? Можно разъяснить? О каком свойстве контрола идёт речь?

          0
          > Зачем же городить огород?
          На этот вопрос вы сами ответите, когда кастомеру понадобится, к пирмеру, очистить содержимое ListBox, но при этом индикатор занятости чтобы был выключен.
          «Мухи отдельно, котледы врозь...»
          Оверхед от дополнительного свойства мизерный по сравнению с теми ограничениями арзитектуры, которые вы огребаете без него.
          И как раз в 99% случаев в разработке корпоративного софта вы ДОЛЖНЫ получить пинок от ревьюеров насчет такого «WTF-code».

          > Тут я не понял, что вы имели в виду? Можно разъяснить? О каком свойстве контрола идёт речь?
          Пусть есть у нас кастомный контрол, для которого мы должны иметь возможность показать индикатор занятости. Заводим в нем свойство BusyIndicator, при выставке которого в сам этот индикатор прокидывается ссылка на «оборачиваемый индикатором контрол» (наш кастомный контрол).
          Этот индикатор должен уметь добавлять оборачиваемому контролу AdornerLayer и закидываться в этот AdornerLayer.
            0
            >> На этот вопрос вы сами ответите, когда кастомеру понадобится, к пирмеру, очистить содержимое ListBox,
            >> но при этом индикатор занятости чтобы был выключен.
            Пустое значение списка — это список с нулевым количеством элементов, а вовсе не null. По крайней мере, я придерживаюсь именно такой концепции.

            > Пусть есть у нас кастомный контрол, для которого мы должны иметь возможность показать индикатор
            Согласен, для кастомного контрола — это правильно, но если речь идёт о стандартных контролах? А в большинстве случаев так оно и есть.
            Хотя можно сделать attached property IsBusy для контрола-селектора.
            >> Этот индикатор должен уметь добавлять оборачиваемому контролу AdornerLayer и закидываться в этот
            >>AdornerLayer.
            Да и индикатор может быть любым, в одном проекте у меня Telerik, в другом DevExpress, в третьем ещё что-то.

            >>И как раз в 99% случаев в разработке корпоративного софта вы ДОЛЖНЫ получить пинок от
            >> ревьюеров насчет такого «WTF-code».
            Тоже согласен, код не очевидный, потому-то мне он в итоге не понравился и я решил переделать его под attached property.
              0
              > Пустое значение списка — это список с нулевым количеством элементов, а вовсе не null. По крайней мере, я придерживаюсь именно такой концепции.
              Очень много крови пролито в холиваре о том, каким должно быть значение по умолчанию дл свойств вида List<ЧегоТоТам>.
              Если у ViewModel по умолчанию значение списка будет null, то везде придется ставить проверку на null. Либо как минимум в геттере свойства. ViewModel должна по умолчанию отдавать значение так, чтобы это не приводило к появлению эксепшнов, т.е. пустую коллекцию/список и т.д. Где список пустой будет создан — в конструкторе или в самом геттере свойства — не важно.

              И еще… вроде если по биндингу в ItemsSource прилетит значение null, то биндинг, вроде, генерит ошибки, которые тихо мирно проглатываются. И вроде бы «и фих с ним», но при ОЧЕНЬ большой частоте возникновения эксепшнов будет серьезно нагружаться GC — в результате страдает первоманс.
                0
                «первоманс» читай «производительность»… Извините.
                «Русский забыл, английский не выучил»

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

      Самое читаемое