Как я могу сохранить отзывчивый пользовательский интерфейс WPF при общении с сервером в другом потоке?

StackOverflow https://stackoverflow.com/questions/1411979

Вопрос

У меня есть клиент 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;
    }

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

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

 //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();
}

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

Это было полезно?

Решение

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

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

public void BeginConnectionIsActive()
{
    AsyncCallback callback = (ar) => 
    {
        bool result = checkConnection.EndInvoke(ar);
        // Do something with the result
    };
    checkConnection.BeginInvoke(callback,null);
}

Другие советы

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

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

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

[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.
    // ...
}

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

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);

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top