«Оживление» пользовательского интерфейса
.NET*

Приложение не отвечает?!
Многие из тех, кто программирует WPF-приложения, наверное тысячи раз писали конструкцию вида:
{Binding Items}
Если получение элементов коллекции Items выполняется в основном потоке приложения и занимает некоторое время — мы получаем «мертвый» пользовательский интерфейс. Приложение некоторое время не будет отрисовывать изменения состояния и реагировать на пользовательский ввод. И если время обработки превысит некоторый лимит времени, определенный в оконной системе Windows — система пометит данное окно как не отвечающее на запросы: на изображение последнего успешного рендеринга окна наложиться белая маска и к заголовку добавиться специальный маркер (Not responding) ((Не отвечает) в русской локализации):

Да вы и сами наверное много раз наблюдали подобную картину, когда какое-нибудь приложение в результате сбоя или выполнения синхронной операции «уходит в себя». И, конечно же, знаете как это раздражает пользователя. Большинство
Решение №1: Асинхронный ObjectDataProvider
Решение очень простое и идеально подойдет тем, кто использует в текущих проектах ObjectDataProvider в качестве источника данных.
Шаг №1: Реализуем простой статический провайдер данных
Провайдер представляет собой обычный статический класс с одним методом:
// Emulates a long items getting process using some delay of getting of each item
public static class AsyncDataProvider
{
private const int _DefaultDelayTime = 300;
public static ReadOnlyCollection<string> GetItems()
{
return GetItems(_DefaultDelayTime);
}
public static ReadOnlyCollection<string> GetItems(int delayTime)
{
List<string> items = new List<string>();
foreach (string item in Enum.GetNames(typeof(AttributeTargets)).OrderBy(item => item.ToLower()))
{
items.Add(item);
// Syntetic delay to emulate a long items getting process
Thread.Sleep(delayTime);
}
return items.AsReadOnly();
}
}Шаг №2: Объявляем асинхронный источник данных в XAML
<Window.Resources>
<ObjectDataProvider x:Key="AsyncDataSource"
IsAsynchronous="True"
ObjectType="Providers:AsyncDataProvider"
MethodName="GetItems" />
<Converters:NullToBooleanConverter x:Key="NullToBooleanConverter" />
</Window.Resources>Конвертер NullToBooleanConverter — это всего лишь вспомогательный объект, назначение которого можно прочесть в названии (его реализацию можно найти в прикрепленном к топику проекте). Вся магия заключается в аттрибуте IsAsynchronous="True" объекта ObjectDataProvider. Этот аттрибут отвечает за управление способом получения данных — если этот аттрибует установлен в "True" ядро WPF создаст для получения значения этого свойства фоновый объект Dispatcher и, таким образом, привязка будет выполнятся в фоновом потоке, не мешая основному потоку приложения обрабатывать пользовательский ввод.
Шаг №3: Используем провайдер данных в коде
<ListBox x:Name="ItemsListBox"
ItemsSource="{Binding Source={StaticResource AsyncDataSource}, IsAsync=True}">
<ListBox.Style>
<Style TargetType="{x:Type ListBox}">
<Style.Triggers>
<Trigger Property="ItemsSource" Value="{x:Null}">
<Setter Property="Template" Value="{StaticResource WaitControlTemplate}" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>Обратите внимание — для списка использован триггер, позволяющий визуализировать для пользователя процесс получения данных. Это очень важно — если не информировать пользователя о какой-то длительной операции, он подумает что приложение просто не работает, поскольку ни одно действие над списком не будет доступно (при правильной обработке их состояний, конечно — об этом дальше).
Шаг №4: Не забываем обрабатывать доступность действий
<Button Grid.Column="1"
Content="Edit"
Width="70"
IsEnabled="{Binding SelectedItem, ElementName=ItemsListBox, Converter={StaticResource NullToBooleanConverter}}"
Click="EditButton_Click"/>
Шаг №5: In action
Вот как выглядит главное окно после запуска со всеми нашими изменениями:

А вот так оно выглядит после того, как данные получены:

Кнопка Edit привязана к выделенному элементу через простой конвертер. Если выделенный элемент в основном списке ItemsListBox отсутствует — кнопка будет недоступна. А выделить элемент можно будет только после того, как асинхронный провайдер данных AsyncDataSource заполнит элементами список. Кнопка Close добавлена для визуализации возможности управления приложением — ничто не мешает нажать на нее во время процесса получения данных и закрыть главное окно. Приложение при этом исправно отреагирует на наш запрос и закроется, чего не произошло бы в том случае, если наш источник данных был бы синхронным.
Решение №2 Асинхронный Binding
Второе решение этой задачи использует паттерн M-V-VM (Model-View-ViewModel), наверное один из популярнейших сейчас паттернов построения модульных приложений для WPF и Silverlight. Обсуждение данного паттерна выходит за рамки данной статьи — при желании вы сможете легко найти о нем много информации в сети (если вам лень искать — загляните в раздел Ссылки в конце статьи).
Шаг №1:
Создадим модель представления для главного окна приложения:
public class MainViewModel
{
private ICommand _commandClose;
private ICommand _commandEdit;
private ReadOnlyCollection<string> _items;
public ReadOnlyCollection<string> Items
{
get
{
if (_items == null)
{
_items = AsyncDataProvider.GetItems();
}
return _items;
}
}
public ICommand CommandClose
{
get
{
if (_commandClose == null)
{
_commandClose = new RelayCommand(p => OnClose());
}
return _commandClose;
}
}
public ICommand CommandEdit
{
get
{
if (_commandEdit == null)
{
_commandEdit = new RelayCommand(p => OnEdit(p), p => CanEdit);
}
return _commandEdit;
}
}
public string SelectedItem
{
get;
set;
}
private void OnClose()
{
App.Current.Shutdown();
}
private void OnEdit(object parameter)
{
MessageBox.Show(String.Format("Edtiting item: {0}",
parameter != null ? parameter.ToString() : "Not selected"));
}
private bool CanEdit
{
get
{
return SelectedItem != null;
}
}
}Шаг №2: Немного изменим объявление привязки в коде XAML главного представления
<ListBox x:Name="ItemsListBox"
Grid.Row="0"
ItemsSource="{Binding Items, IsAsync=True}"
SelectedItem="{Binding SelectedItem}">
<ListBox.Style>
<Style TargetType="{x:Type ListBox}">
<Style.Triggers>
<Trigger Property="ItemsSource" Value="{x:Null}">
<Setter Property="Template" Value="{StaticResource WaitControlTemplate}" />
</Trigger>
</Style.Triggers>
</Style>
</ListBox.Style>
</ListBox>В этом сценарии за асинхронность отвечает аттрибут, указанный в разметке привязки: "{Binding Items, IsAsync=True}". Как и в примере с ObjectDataProvider ядро WPF создаст отдельный фоновый диспетчер для получения значения привязки в отдельном асинхронном контексте.
Отдельно стоит отметить что в этом сценарии нам не требуется прибегать к кодированию правил видимости элементов управления в XAML-коде представления главного окна. В приведенном выше коде модели представления за видимость кнопки Edit на форме отвечает свойство MainViewModel.CanEdit, которое является частью команды MainViewModel.CommandEdit. Более детально узнать о паттерне Команда (Command) вы можете, заглянув в раздел Ссылки. Здесь же будет уместно заметить лишь то, что нам не придется ничего делать вручную — обо всем позаботится класс CommandManager. От нас требуется лишь правильная реализация контракта ICommand, которую обеспечивает класс RelayCommand (с реализацией этого класса вы можете ознакомится в прилагаемом проекте).
Домашнее задание
Выполняется по желанию — проверять не буду, даже не просите :) Можно слегка усовершенствовать шаблон WaitControlTemplate превратив его в полноценный элемент управления, унаследованный от класса Border или от его предка Decorator, если есть желание сделать полноценный элемент управления по всем правилам. Поведение и логика этого элемента будут простыми:
- Внутрь элемента можно добавить только ItemsControl или любого из его наследников (ListBox, ListView, TreeView etc) — контролировать это желательно на самом жестком уровне, вплоть до выбрасывания исключения если свойство Content не является ItemsControl
- При изменении свойства Content элемент управления пытается привести содержимое к ItemsControl и получить значение привязки данных свойства ItemsSource
- Если предыдущий шаг удался — перевести привязку в асинхронный режим
- Визуализация элемента управления может быть частично построена на логике паттерна Заместитель (Proxy) — пока данные асинхронно загружаются элемент управления показывает свое содержимое (крутящийся индикатор загрузки и надпись с просьбой подождать), после завершения загрузки отображается содержимое свойства Content
Резюме
Главными клиентами разработчиков являются пользователи. И каждому разработчику время от времени полезно ставить себя на место простого пользователя, проводить полный цикл тестирования своего приложения и пытаться анализировать, что именно его (как пользователя) раздражает и что бы он (как пользователь) хотел бы улучшить.
Исходный код
AsyncBinding.zip
Ссылки
Приложения WPF с шаблоном проектирования модель-представление-модель представления
Общие сведения о системе команд
Asynchronous Data Binding in Windows Presentation Foundation
Asynchronous WPF
Оригинал главного изображения для топика взят отсюда

комментарии (10)