Как я могу поддерживать отзывчивость моего интерфейса WPF при общении с сервером в другом потоке? - PullRequest
1 голос
/ 11 сентября 2009

У меня есть клиент wcf, который перед выполнением сложных взаимодействий со службой wcf выполняет простую проверку работоспособности службы. Поскольку время ожидания сбойного соединения составляет одну минуту, я хочу выполнить эту проверку асинхронно.

Вот что у меня есть:

//Method on the main UI thread
public void DoSomeComplicatedInteraction()
{
  If(ConnectionIsActive()) DoTheComplicatedInteraction();
}


 private bool ConnectionIsActive()
    {
        //Status is a Textblock on the wpf form
        Status.Text = "Checking Connection";
        var ar = BeginConnectionIsActive();
        while (!ar.IsCompleted)
        {
            Status.Text += ".";
            Thread.Sleep(100);
        }
        EndConnectionIsActive(ar);
        //IsConnected is a boolean property
        return IsConnected;
    } 

   private IAsyncResult BeginConnectionIsActive()
    {
        //checkConnection is Func<bool> with value CheckConnection
        return checkConnection.BeginInvoke(null,checkConnection);
    }


    private void EndConnectionIsActive(IAsyncResult ar)
    {
        var result=((Func<bool>)ar.AsyncState).EndInvoke(ar);
        IsConnected = result;
    }

  private bool CheckConnection()
    {
        bool succes;
        try
        {
            //synchronous operation
            succes=client.Send(RequestToServer.GetPing()).Succes;
        }
        catch
        {
            succes = false;
        }
        return succes;
    }

Это работает. Только когда я пытаюсь смоделировать медленный ответ сервера, добавив Thread.Sleep в метод Send сервера, пользовательский интерфейс перестает отвечать на запросы. Более того, текст текстового блока статуса не обновляется. Кажется, мне нужен какой-то метод Application.DoEvents. Или мне нужен другой подход?

EDIT: Действительно, нужен другой подход. Использование Thread.Sleep блокирует пользовательский интерфейс, когда он вызывается в основном потоке пользовательского интерфейса. Вот как я это решил:

 //Method on the main UI thread
public void DoSomeComplicatedInteraction()
{
  IfConnectionIsActiveDo(TheComplicatedInteraction);
}

   private void TheComplicatedInteraction()
    {...}

  private void IfConnectionIsActiveDo(Action action)
    {
        Status.Text = "Checking Connection";
        checkConnection.BeginInvoke(EndConnectionIsActive,
        new object[] { checkConnection, action });
    }

private void EndConnectionIsActive(IAsyncResult ar)
{
    var delegates = (object[]) ar.AsyncState;
    var is_active_delegate = (Func<bool>) delegates[0];
    var action = (Action) delegates[1];
    bool is_active=is_active_delegate.EndInvoke(ar);
    IsConnected = is_active;
    Dispatcher.Invoke(DispatcherPriority.Normal,
      new Action<bool>(UpdateStatusBarToReflectConnectionAttempt),is_active);
    if (is_active) action();
}

Я не очень доволен этим: он распространяет (синхронный) код, который раньше был двумя методами, на пять! Это делает код очень грязным ...

Ответы [ 3 ]

3 голосов
/ 11 сентября 2009

Это потому, что вы ожидаете в цикле завершения операции. Этот цикл выполняется в основном потоке, поэтому пользовательский интерфейс заморожен. Вместо этого вам следует передать AsyncCallback в BeginInvoke (вместо нуля), чтобы вы могли получать уведомления о завершении операции.

Вот как я бы это реализовал:

public void BeginConnectionIsActive()
{
    AsyncCallback callback = (ar) => 
    {
        bool result = checkConnection.EndInvoke(ar);
        // Do something with the result
    };
    checkConnection.BeginInvoke(callback,null);
}
2 голосов
/ 11 сентября 2009

Я не могу точно сказать из вашего примера кода, где вы вызываете это, но в целом вы не должны никогда блокировать работу в потоке пользовательского интерфейса. Это заставит пользовательский интерфейс перестать отвечать на запросы, как вы упоминаете. Вы должны сделать эту работу в фоновом режиме. Я предполагаю, что вы запускаете этот цикл ConnectionIsActive непосредственно в потоке пользовательского интерфейса, что означает, что обновления состояния пользовательского интерфейса будут по существу заблокированы до тех пор, пока вы не вернетесь из этого метода.

Похоже, у вас правильная идея (с использованием асинхронных методов), но реализация кажется слишком сложной.

Удобно, WCF позволяет вам иметь асинхронный контракт на стороне клиента, даже если реализация сервера не использует асинхронные методы. Контракты просто должны иметь одинаковое имя контракта и быть помечены специально для асинхронного шаблона, как показано в этом примере:

[ServiceContract(Name = "Ping")]
interface IPing
{
    [OperationContract(IsOneWay = false)]
    void Ping(string data);
}

[ServiceContract(Name = "Ping")]
interface IPingClient
{
    [OperationContract(AsyncPattern = true, IsOneWay = false)]
    IAsyncResult BeginPing(string data, AsyncCallback callback, object state);

    void EndPing(IAsyncResult result);
}

Теперь на стороне клиента вы можете использовать IPingClient контракт и просто позвонить client.BeginPing(...). Он сразу же вернется, пока вся фактическая работа будет выполнена в фоновом режиме, при необходимости перезвонит вам после завершения.

Ваш код может выглядеть примерно так:

void SomeCodeInUIThread()
{
    // ...
    // Do something to the UI to indicate it is
    // checking the connection...

    client.BeginPing("...some data...", this.OnPingComplete, client);
}

void OnPingComplete(IAsyncResult result)
{
    IPingClient client = (IPingClient)result.AsyncState;
    try
    {
        client.EndPing(result);
        // ...
    }
    catch (TimeoutException)
    {
        // handle error
    }

    // Operation is complete, so update the UI to indicate
    // you are done. NOTE: You are on a callback thread, so
    // make sure to Invoke back to the main form.
    // ...
}
1 голос
/ 12 сентября 2009

Используйте что-то вроде этого:

public static class Run
{
    public static void InBackround<TResult>(Func<TResult> operation, Action<TResult> after)
    {
        Async(operation, after, DispatcherPriority.Background);
    }

    private static void Async<TResult>(Func<TResult> operation, Action<TResult> after,
                                               DispatcherPriority priority)
    {
        var disp = Dispatcher.CurrentDispatcher;

        operation.BeginInvoke(delegate(IAsyncResult ar)
        {
            var result = operation.EndInvoke(ar);
            disp.BeginInvoke(priority, after, result);
        }, null);
    }
}

И назовите это так:

Run.InBackround(CheckConnection, DoTheComplicatedInteraction);

Он принимает некоторый делегат, вызывает его в фоновом потоке, и после его окончания получает возвращаемое значение и вызывает делегат после элемента в потоке пользовательского интерфейса.

В приведенном выше примере DoTheComplicatedInteraction должно выглядеть следующим образом:

DoTheComplicatedInteraction(bool connected) {
    if(connected) { proceed... }  else { retry... }
}

Если вы не уверены в Dispatcher, прочитайте Создание более отзывчивых приложений с помощью статьи Dispatcher на msdn.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...