При разработке пользовательского интерфейса очень часто возникают ситуации, когда требуется получить некторые данные из различных источников (база данных, web-сервис, wcf-сервис и т.д.). При этом хорошим тоном будет отображать пользователю, что приложение выполняет запрос, а не "вешать" приложение до окончания выполнения операции. Соответственно выход из этой ситуации - запускать процессы получения данных в отдельных потоках.
Помимо того, что иногда будет требоваться передать данные в поток, так их еще нужно получить обратно в UI-поток и обработать, если есть, возникшие во время запроса ошибки. Задача не из простых. Но если использовать класс
Task из .NET Framework 4, то задача немного упрощается, так как специально для этого класса я сделал несколько методов расширений, которые очень помогают в решении вышеописанных задач:
TaskExtensions.cs:
public static class TaskExtensions
{
#region Public Members
public static Task ToAsync(this Task task, AsyncCallback callback, object state)
{
var taskCompletionSource = new TaskCompletionSource<object>(state);
task.ContinueWith(delegate
{
if (task.IsFaulted)
{
if (task.Exception != null) taskCompletionSource.TrySetException(task.Exception.InnerExceptions);
}
else if (task.IsCanceled)
{
taskCompletionSource.TrySetCanceled();
}
else
{
taskCompletionSource.TrySetResult(null);
}
if (callback != null) callback(taskCompletionSource.Task);
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
return taskCompletionSource.Task;
}
public static Task ToAsync(this Task task, AsyncCallback callback, object state, TaskScheduler taskScheduler)
{
var taskCompletionSource = new TaskCompletionSource<object>(state);
task.ContinueWith(delegate
{
if (task.IsFaulted)
{
if (task.Exception != null) taskCompletionSource.TrySetException(task.Exception.InnerExceptions);
}
else if (task.IsCanceled)
{
taskCompletionSource.TrySetCanceled();
}
else
{
taskCompletionSource.TrySetResult(null);
}
if (callback != null) callback(taskCompletionSource.Task);
}, CancellationToken.None, TaskContinuationOptions.None, taskScheduler);
return taskCompletionSource.Task;
}
public static Task<TResult> ToAsync<TResult>(this Task<TResult> task, AsyncCallback callback, object state)
{
var taskCompletionSource = new TaskCompletionSource<TResult>(state);
task.ContinueWith(delegate
{
if (task.IsFaulted)
{
if (task.Exception != null) taskCompletionSource.TrySetException(task.Exception.InnerExceptions);
}
else if (task.IsCanceled)
{
taskCompletionSource.TrySetCanceled();
}
else
{
taskCompletionSource.TrySetResult(task.Result);
}
if (callback != null) callback(taskCompletionSource.Task);
}, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default);
return taskCompletionSource.Task;
}
public static Task<TResult> ToAsync<TResult>(this Task<TResult> task, AsyncCallback callback, object state, TaskScheduler taskScheduler)
{
var taskCompletionSource = new TaskCompletionSource<TResult>(state);
task.ContinueWith(delegate
{
if (task.IsFaulted)
{
if (task.Exception != null) taskCompletionSource.TrySetException(task.Exception.InnerExceptions);
}
else if (task.IsCanceled)
{
taskCompletionSource.TrySetCanceled();
}
else
{
taskCompletionSource.TrySetResult(task.Result);
}
if (callback != null) callback(taskCompletionSource.Task);
}, CancellationToken.None, TaskContinuationOptions.None, taskScheduler);
return taskCompletionSource.Task;
}
#endregion
}
Данный класс похож на один из
Samples for Parallel Programming with the .NET Framework, но у него есть нюанс, о котором я расскажу чуть позже. Описывать сами методы я не буду (так как класс снабжен исчерпывающим количеством комментариев), но как их можно использовать я покажу.
Предположим, что у нас есть некий интерфейс доступа к данным:
:
public interface IDataAccess
{
Task<IEnumerable<Object>> GetItems(long id);
}
И есть коллекция объектов, которая отображается в интерфейсе пользователя и заполняется данными, полученными из метода
GetItems(long id). Теперь, чтобы получить эти данные, проверить есть ли ошибки и отобразить список, нужно сделать следующее:
Code:
public ObservableCollection<Object> Items { get; private set; }
private void CommandExecute(long id)
{
IDataAccess dataAccess = new DataAccess();
dataAccess.GetItems(id).ToAsync(Callback, null);
}
private void Callback(IAsyncResult asyncResult)
{
var task = asyncResult as Task<IEnumerable<Object>>;
if (task == null) return;
switch (task.Status)
{
case TaskStatus.RanToCompletion:
foreach (var item in task.Result)
{
var model = item;
Application.Current.Dispatcher.Invoke(DispatcherPriority.DataBind, new Action(() => Items.Add(model)));
}
break;
case TaskStatus.Faulted:
if (task.Exception != null) task.Exception.Handle(ExceptionHandle);
break;
}
}
Здесь в методе
CommandExecute(long id) мы создаем экземпляр интерфейса и вызываем его метод. Затем применяем метод расширения
ToAsync(Callback, null), где в качестве первого параметра передаем метод, который выполнится по завершению асинхронной операции, а в качестве второго параметра - объект состояния (сюда можно сохранить объект, который потребуется в методе обратного вызова). В самом методе
Callback(IAsyncResult asyncResult) мы проверяем состояние выполненной задачи и если все отлично, добавляем элементы в список, если же овозникла ошибка, то запускаем обработчик. Так как этот метод выполняется в отдельном потоке, то для доступа к коллекции
Items приходится использовать объект
Dispatcher, чтобы обновить интерфейс пользователя. Получается неплохо!
А теперь еще одна изюминка этих методов расширения: если вызов асинхронного метода изменить следующим образом:
Code:
private void CommandExecute(long id)
{
IDataAccess dataAccess = new DataAccess();
dataAccess.GetItems(id).ToAsync(Callback, null, TaskScheduler.FromCurrentSynchronizationContext());
}
то
Callback(IAsyncResult asyncResult)
Это достигается благодаря
TaskScheduler.FromCurrentSynchronizationContext(), который запускает метод обратного вызова в пользовательском потоке. Тем самым необходимость использовать объект
Dispatcher
отпадает.
Спасибо за подсказку, данная статья помогла решить вопрос многопоточной загрузки картинок.
ОтветитьУдалитьПожалуйста! Рад, что данная статья вам помогла :)
ОтветитьУдалить