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

в 5:53, , рубрики: .net, binding, silverlight, wpf, XAML, разработка, метки: , , , ,

Использование такого компонента как 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 шаг за шагом я расскажу, что у меня получилось.

Спасибо тем кто дочитал до конца.

Автор: LionSoft


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js