他のスレッドのサーバーと通信中にWPF UIの応答性を維持するにはどうすればよいですか?
-
05-07-2019 - |
質問
wcfサービスとの複雑なやり取りを行う前に、サービスが動作していることを簡単に確認するwcfクライアントがあります。失敗した接続のタイムアウトは1分なので、このチェックを非同期に実装します。
これは私が持っているものです:
//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;
}
これは動作します。のみ、サーバーのSendメソッドにThread.Sleepを追加して、サーバーの遅い応答をシミュレートしようとすると、UIが応答しなくなります。さらに、ステータステキストブロックのテキストは視覚的に更新されません。 何らかの種類のApplication.DoEventsメソッドが必要なようです。 または、別のアプローチが必要ですか?
編集: 実際、別のアプローチが必要です。 Thread.Sleepを使用すると、メインUIスレッドで呼び出されたときにUIがブロックされます。 解決方法は次のとおりです。
//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();
}
これにはあまり満足していません。以前は2つのメソッドにあった(同期)コードを5つに広げています!これにより、コードが非常に乱雑になります...
解決
これは、操作の完了をループで待機しているためです。このループはメインスレッドで実行されるため、UIはフリーズします。代わりに、 AsyncCallback
を BeginInvoke
に(nullではなく)渡す必要があります。これにより、操作の完了時に通知を受けることができます。
これを実装する方法は次のとおりです。
public void BeginConnectionIsActive()
{
AsyncCallback callback = (ar) =>
{
bool result = checkConnection.EndInvoke(ar);
// Do something with the result
};
checkConnection.BeginInvoke(callback,null);
}
他のヒント
コードサンプルからこれを呼び出している場所を確認することはできませんが、一般的には、UIスレッドでブロッキング処理を実行しないでください。あなたが言及したように、これはUIが応答しなくなる原因になります。この作業はバックグラウンドで実行する必要があります。私の推測では、ConnectionIsActiveループをUIスレッドで直接実行しているため、UIステータスの更新は、そのメソッドから戻るまで基本的にブロックされます。
(非同期メソッドを使用して)正しい考えを持っているように見えますが、実装は過度に複雑に見えます。
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.
// ...
}
次のようなものを使用します:
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);
いくつかのデリゲートを取り、バックグラウンドスレッドで呼び出し、終了後に戻り値を取得し、UIスレッドで 'after'デリゲートを呼び出します。
上記の例では、DoTheComplicatedInteractionは次のようになります。
DoTheComplicatedInteraction(bool connected) {
if(connected) { proceed... } else { retry... }
}
Dispatcherについて混乱している場合は、 Dispatcherで応答性の高いアプリを構築してください msdnの記事。