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