Я как-то уже затрагивал тему асинхронной загрузки данных в своей статье TaskExtensions в помощь. Однако, используя такой подход, код со временем начинал обрастать огромным количеством callback'ов и командами. Чтобы оптимизировать данный процесс для своих нужд я сделал класс, которым и хочу поделиться. Ничего нового в нем нет, однако, он позволил сократить огромное количество кода и на мой взгляд является весьма интересным.
Суть класса такова: предоставить пользователю коллекцию динамических данных с выдачей уведомлений при обновлении, а также обеспечить механизм индикации загрузки данных, при этом выбор источника данных предоставить потребителю.
Самый классический сценарий для UI-девелопера: список объектов и кнопка "Обновить". Я решил совместить это в одной коллекции:
Самый классический сценарий для UI-девелопера: список объектов и кнопка "Обновить". Я решил совместить это в одной коллекции:
public sealed class AsyncObservableCollection<T> : ObservableCollection<T>, ICommand
{
private readonly Func<Object, Task<IEnumerable<T>>> factory;
public AsyncObservableCollection(Func<Object, Task<IEnumerable<T>>> factory)
{
this.factory = factory;
}
}
Как видно - это обычная обобщенная коллекция, которая наследует интерфейс ICommand (не совсем стандартный подход). В качестве параметра конструктор класса принимает делегат, который по сути является фабрикой, экземпляр которого будет вызываться для получения списка объектов асинхронно. Тем самым я предоставил пользователю (потребителю) класса самому решать как и откуда он будет загружать данные.
Для обеспечения механизма индикации потребуется добавить несколько свойств (хочу внести ясность, что под индикацией я понимаю отображение любой информации пользователю о выполнении асинхронной операции, будь-то busy-индикатор или обычный текст в status bar):
Назначение этих свойств, я думаю, понятно из названий, но вот почему они ссылаются на целое значение (а не на булево), я объясню это на коде реализации интерфейса ICommand:
Поскольку метод ICommand.Execute теоретически может вызываться несколько раз подряд и к тому же из разных потоков, а предыдущее обновление элементов списка возможно еще не завершилось, то здесь я выполняю сравнение значения переменной loading. Если loading == 0, то есть загрузка данных не выполняется, я меняю значение loading = 1 и запускаю обновление списка. Иначе, если loading == 1, то ничего не делаем, так как список находится в процессе обновления.
Операция сравнения и изменения значения переменной loading выполняется атомарно благодаря статическому методу Interlocked.CompareExchange (к сожалению перегрузки для работы с типом bool нет), который позволяет выполнять данные операции с переменными, общедоступными нескольким потокам.
Теперь приведу код метода InternalExecute, который обрабатывает индикацию выполнения и запускает задачу загрузки данных:
С помощью CheckAccess я проверяю, связан ли вызывающий поток с UI-потоком. Если да, то обновляю состояние индикации и запускаю задачу для загрузки данных. Если же нет, то выполняю этот же метод в контексте UI-потока. Все это сделано для того, чтобы безо всяких сюрпризов обновить свойства индикации, которые могут быть привязаны к элементам управления в пользовательском интерфейсе.
Про метод ToAsync я рассказывал в статье TaskExtensions в помощь, а вот про метод обратного вызова Callback расскажу немного подробней:
private Int32 loading;
public Boolean IsLoading
{
get { return loading != 0; }
}
public Boolean IsEnabled
{
get { return !IsLoading; }
}
Назначение этих свойств, я думаю, понятно из названий, но вот почему они ссылаются на целое значение (а не на булево), я объясню это на коде реализации интерфейса ICommand:
private EventHandler canExecuteChangedDelegate;
event EventHandler ICommand.CanExecuteChanged
{
add { canExecuteChangedDelegate += value; }
remove { canExecuteChangedDelegate -= value; }
}
bool ICommand.CanExecute(object parameter)
{
return IsEnabled;
}
void ICommand.Execute(object parameter)
{
if (factory == null) return;
if (Interlocked.CompareExchange(ref loading, 1, 0) == 0)
InternalExecute(parameter);
}
Поскольку метод ICommand.Execute теоретически может вызываться несколько раз подряд и к тому же из разных потоков, а предыдущее обновление элементов списка возможно еще не завершилось, то здесь я выполняю сравнение значения переменной loading. Если loading == 0, то есть загрузка данных не выполняется, я меняю значение loading = 1 и запускаю обновление списка. Иначе, если loading == 1, то ничего не делаем, так как список находится в процессе обновления.
Операция сравнения и изменения значения переменной loading выполняется атомарно благодаря статическому методу Interlocked.CompareExchange (к сожалению перегрузки для работы с типом bool нет), который позволяет выполнять данные операции с переменными, общедоступными нескольким потокам.
Теперь приведу код метода InternalExecute, который обрабатывает индикацию выполнения и запускает задачу загрузки данных:
private void InternalExecute(Object parameter)
{
if (Application.Current.CheckAccess())
{
OnCanExecuteChanged();
Clear();
ToAsync(factory(parameter), Callback, parameter);
}
else
{
Application.Current.Dispatcher.Invoke(DispatcherPriority.DataBind,
new Action<Object>(InternalExecute), parameter);
}
}
private void OnCanExecuteChanged()
{
OnPropertyChanged(new PropertyChangedEventArgs("IsLoading"));
OnPropertyChanged(new PropertyChangedEventArgs("IsEnabled"));
var handler = canExecuteChangedDelegate;
if (handler == null) return;
handler(this, EventArgs.Empty);
}
С помощью CheckAccess я проверяю, связан ли вызывающий поток с UI-потоком. Если да, то обновляю состояние индикации и запускаю задачу для загрузки данных. Если же нет, то выполняю этот же метод в контексте UI-потока. Все это сделано для того, чтобы безо всяких сюрпризов обновить свойства индикации, которые могут быть привязаны к элементам управления в пользовательском интерфейсе.
Про метод ToAsync я рассказывал в статье TaskExtensions в помощь, а вот про метод обратного вызова Callback расскажу немного подробней:
private void Callback(IAsyncResult asyncResult)
{
var task = asyncResult as Task<IEnumerable<T>>;
if (task == null) return;
switch (task.Status)
{
case TaskStatus.RanToCompletion:
// The task completed execution successfully.
Action<T> action = Add;
foreach (var entity in task.Result)
{
var obj = entity;
Application.Current.Dispatcher.Invoke(
DispatcherPriority.DataBind, action, obj);
}
break;
case TaskStatus.Faulted:
// The task completed due to an unhandled exception.
if (task.Exception != null) task.Exception.Handle(ExceptionHandle);
break;
}
Thread.VolatileWrite(ref loading, 0);
Application.Current.Dispatcher.Invoke(DispatcherPriority.DataBind,
new Action(OnCanExecuteChanged));
}
Этот метод вызывается после выполнения пользовательского делегата загрузки данных, и проверяет состояние задачи. Если выполнение прошло без ошибок - добавляем элементы в коллекцию и в конце, с помощью статического метода Thread.VolatileWrite устанавливаем состояние индикации в начальное. Так же не забывайте обрабатывать ошибки, которые могли произойти в пользовательском делегате, так как это может быть причиной ошибки в дальнейшем (кстати, знаете почему?).
Ну вот основные моменты класса я описал. Ссылку на полный исходный код можно найти в конце статьи. Осталось показать как это применить на практике.
MainView:
MainViewModel:
Обратите внимание, что в качестве аргумента мы можем передать любое значение, которое нам потребуется. В примере передается количество элементов которое требуется получить. И еще один момент: привязка кнопки к команде выглядит немного странно Command = "{Binding Items}", все потому, что мой класс наследует интерфейс ICommand.
P.S. Чтобы "увидеть" динамическое обновление интерфейса, вы можете в методе обратного вызова Callback в цикле foreach сделать небольшую задержку, к примеру Thread.Sleep(100), для того, чтобы список успевал обновляться.
Исходный код
MainView:
<Window x:Class="WpfCSharpTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:s="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:WpfCSharpTest"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<s:BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter"/>
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="36"/>
</Grid.RowDefinitions>
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Grid.Column="0"/>
<TextBlock Text="{Binding Description}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Text="Загрузка данных..." VerticalAlignment="Center" HorizontalAlignment="Left" Grid.Row="1" Margin="10,0"
Visibility="{Binding Items.IsLoading, Converter={StaticResource booleanToVisibilityConverter}}"/>
<TextBox x:Name="textBox" Text="100000" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Row="1"/>
<Button Content="Обновить" Command="{Binding Items}" CommandParameter="{Binding Text, ElementName=textBox}"
VerticalAlignment="Center" HorizontalAlignment="Right" Grid.Row="1" Margin="10,0"/>
<Grid>
<Window>
MainViewModel:
class MainViewModel
{
public MainViewModel()
{
Items = new AsyncObservableCollection<CustomItem>(Factory);
}
public AsyncObservableCollection<CustomItem> Items { get; private set; }
private static Task<IEnumerable<CustomItem>> Factory(object parameter)
{
return Task<IEnumerable<CustomItem>>.Factory.StartNew(delegate(object o)
{
var count = Int32.Parse(o.ToString());
var list = new List<CustomItem>();
for (var i = 0; i < count; i++)
{
list.Add(new CustomItem
{
Name = "Name " + i,
Description = "Description " + i
});
}
return list;
}, parameter);
}
}
class CustomItem
{
public String Name { get; set; }
public String Description { get; set; }
}
Обратите внимание, что в качестве аргумента мы можем передать любое значение, которое нам потребуется. В примере передается количество элементов которое требуется получить. И еще один момент: привязка кнопки к команде выглядит немного странно Command = "{Binding Items}", все потому, что мой класс наследует интерфейс ICommand.
P.S. Чтобы "увидеть" динамическое обновление интерфейса, вы можете в методе обратного вызова Callback в цикле foreach сделать небольшую задержку, к примеру Thread.Sleep(100), для того, чтобы список успевал обновляться.
Исходный код
Комментариев нет:
Отправить комментарий