• Microsoft .NET
  • C#.NET
  • Решение 11 распространенных проблем в многопоточном коде

Асинхронный вызов метода - Использование шаблона команды

ОГЛАВЛЕНИЕ

Использование шаблона команды ради аккуратности

Появляется путаница. Всюду есть 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, поэтому не перебарщивайте с этим!
•    При использовании обратного вызова сопровождающий его неприятный код скрывается с помощью шаблона команды.
•    Пользовательский интерфейс должен блокироваться только при работе с ним.