Асинхронный вызов метода - Использование шаблона команды
ОГЛАВЛЕНИЕ
Использование шаблона команды ради аккуратности
Появляется путаница. Всюду есть BeginInvoke, EndInvoke и обратный вызов. Простой и удобный шаблон команды очищает вызов метода. Создается объект команды, реализующий следующий простой интерфейс:
public interface ICommand
{
void Execute();
}
Пора перестать использовать везде бесполезную функцию Foo и сделать нечто более реальное. Создается более реалистичный сценарий. Скажем, имеется следующее:
• Пользовательская форма, содержащая сетку, отображающую строки клиента.
• Сетка обновляется строками на основе критерия поиска идентификатора клиента. Однако база данных расположена далеко, и получение набора данных клиента занимает 5 секунд; нельзя блокировать пользовательский интерфейс на время ожидания.
• Имеется приятный бизнес-объект, получающий набор данных клиента на основе идентификатора клиента.
Допустим, это был слой бизнес-логики. Для упрощения примера жестко задано то, что обычно поступает из слоя данных.
public class BoCustomer
{
public DataSet GetCustomer(int intCustomerId)
{
// вызвать слой данных и получить информацию и клиенте
DataSet ds = new DataSet();
DataTable dt = new DataTable("Customer");
dt.Columns.Add("Id", typeof(int));
dt.Columns.Add("FirstName", typeof(string));
dt.Columns.Add("LastName", typeof(string));
dt.Rows.Add(intCustomerId, "Mike", "Peretz");
ds.Tables.Add(dt);
// сделать это долгим...
System.Threading.Thread.Sleep(2000);
return ds;
}
}
Создается команда, обновляющая сетку на основе идентификатора клиента.
public class GetCustomerByIdCommand : ICommand
{
private GetCustomerByIdDelegate m_invokeMe;
private DataGridView m_grid;
private int m_intCustmerId;
// делегат закрытый,
// только команда может использовать его.
private delegate DataSet GetCustomerByIdDelegate(int intCustId);
public GetCustomerByIdCommand(BoCustomer boCustomer,
DataGridView grid,
int intCustId)
{
m_grid = grid;
m_intCustmerId = intCustId;
// установить делегат для вызова
m_invokeMe =
new GetCustomerByIdDelegate(boCustomer.GetCustomer);
}
public void Execute()
{
// вызвать метод в пуле потоков
m_invokeMe.BeginInvoke(m_intCustmerId,
this.CallBack, // обратный вызов
null);
}
private void CallBack(IAsyncResult ar)
{
// получить набор данных как вывод
DataSet ds = m_invokeMe.EndInvoke(ar);
// потокобезопасно обновить сетку
MethodInvoker updateGrid = delegate
{
m_grid.DataSource = ds.Tables[0];
};
if (m_grid.InvokeRequired)
m_grid.Invoke(updateGrid);
else
updateGrid();
}
}
GetCustomerByIdCommand принимает всю информацию, требуемую ему для выполнения команды.
• Обновляемая сетка.
• Идентификатор клиента для поиска.
• Ссылка на слой бизнес-логики.
Делегат спрятан внутри объекта команды, поэтому клиенту не нужно знать внутреннее устройство команды. Клиенту остается построить команду и вызвать Execute для нее. Асинхронный вызов методов производится в ThreadPool, и неразумно обновлять пользовательский интерфейс из ThreadPool или любого другого потока, отличного от потока пользовательского интерфейса. Для решения данной проблемы эта реализация прячется внутри команды, и проверяется на основе сетки, является ли InvokeRequired() истиной. Если это истина, с помощью Control.Invoke проверяется, что вызов направлен потоку пользовательского интерфейса. (Используются средства создания безымянных методов .NET 2.0.). Ниже показано, как форма создает команду и исполняет ее.
private ICommand m_cmdGetCustById;
private void button1_Click(object sender, EventArgs e)
{
// получить идентификатор клиента с экрана
int intCustId = Convert.ToInt32(m_txtCustId.Text);
// использовать слой бизнес-логики для получения данных
BoCustomer bo = new BoCustomer();
// создать команду для обновления сетки
m_cmdGetCustById = new GetCustomerByIdCommand(
bo, m_grid, intCustId);
// вызвать команду в неблокирующем режиме.
m_cmdGetCustById.Execute();
}
Execute неблокирующий, но прежде чем создать массу классов команды, надо учесть следующее:
• Шаблон команды может вызвать бурный рост числа классов, поэтому применяйте его разумно.
• Было бы легко создать базовый класс для команды с логикой для потокобезопасного обновления сетки, но пример был оставлен простым.
• Можно было бы передать TextBox в объект команды, чтобы он захватил ввод более динамично и позволил вызвать команду в любое время без ее воссоздания.
• Делегат, BeginInvoke, EndInvoke, обратный вызов и код для обеспечения потокобезопасного обновления пользовательского интерфейса инкапсулированы в команду, что хорошо.
Заключение
В статье были рассмотрены все важные аспекты вызова метода в неблокирующем режиме. Надо помнить следующее:
• Делегаты будут содержать правильную сигнатуру для BeginInvoke и EndInvoke. Все выходные параметры и исключения выходят при вызове EndInvoke.
• При использовании BeginInvoke отнимаются ресурсы у ThreadPool, поэтому не перебарщивайте с этим!
• При использовании обратного вызова сопровождающий его неприятный код скрывается с помощью шаблона команды.
• Пользовательский интерфейс должен блокироваться только при работе с ним.