Hi,
meine WinForm Anwendung hat ein Login Formular, beim Click auf OK wird versucht die angegebene Server URL zu kontaktieren/verbinden. Während versucht wird zu verbinden friert mir die Anwendung ein.
Nun möchte ich den Verbindungsaufbau in einen anderen Thread legen. Das habe ich mit einem Backgroundworker gelöst.
public void okButton_Click(object sender, EventArgs e)
{
_logonWorker = new BackgroundWorker();
_logonWorker.DoWork += new DoWorkEventHandler(logonWorker_DoWork);
_logonWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(logonWorker_WorkCompleted);
_logonWorker.WorkerSupportsCancellation = true;
_logonWorker.RunWorkerAsync();
}
private void cancelButton_Click(object sender, EventArgs e)
{
_logonWorker = new BackgroundWorker();
if (_logonWorker.IsBusy)
{
_logonWorker.CancelAsync();
}
}
im DoWork
void logonWorker_DoWork(object sender, DoWorkEventArgs e)
{
if (_logonWorker.CancellationPending)
{
e.Cancel = true;
return;
}
try
{
ServiceAddress = LogInForm.Instance.ServerPath;
Service.Users.LogOn(LogInForm.Instance.UserName, LogInForm.Instance.Password);
}
catch(Exceptions ex){}
}
void logonWorker_WorkCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
throw e.Error;
}
}
Nun scheint sich das ganze so zu verhalten, das einmal die Aufforderung zur Verbindung abgesetzt wurde, diese trotz Canceln des Backgroundworker weiter läuft, und irgendwann dann den Error im Hintergrund zurückgibt das nicht verbunden werden konnte, bzw. Server/Url nicht erreichbar.
Das gleiche habe ich ncoh mit einer SQL Connection, da dauert es auch "ewig" bis ich die Response bekomme das der Server nicht erreichbar ist, dort versuche ich ein
try
{
connection.Open();
}
Was müsste ich machen damit der abgesetzte Verbindungsversuch auch abgebrochen wird, und nicht trotz abbrechen im Hintergrgund weiter versucht wird den Server zu finden ?
Mit Thread arbeiten statt Backgroundworker? Und dann versuchen den Thread bei Abbrechen zu killen?
Danke
Hallo,
deinem Code nach hat das Canceln des Verbindungsaufbau's genau folgenden Effekt: gar keinen.
Ich weiß nicht, wie der Service aussieht zu dem du dich verbinden möchtest. Aber sofern es von der Schnittstelle her nix zum Abbrechen gibt wird da auch nichts abgebrochen werden.
Wissen ist nicht alles. Man muss es auch anwenden können.
PS Fritz!Box API - TR-064 Schnittstelle | PS EventLogManager |
Hmm. Okay. Ich weiß grad nicht ob die Schnittstelle zum Service was zum Abbrechen bereit stellt...
Was wäre denn bei dem SQL DB Beispiel?
void logonWorker_DoWork(object sender, DoWorkEventArgs e)
{
connection = new SqlConnection(connString);
connection.Open();
}
private void cancelButton_Click(object sender, EventArgs e)
{
if (_logonWorker.IsBusy)
{
_logonWorker.CancelAsync();
connection.Close();
}
}
Klicke ich hier auf Abbrechen passiert auch "nichts", im Hintergrund läuft es weiter bis nach einer Minute oder zwei die Exception kommt , TimeOut abgelaufen, SQL Server nicht erreichbar ...
Wie würde ich in diesem Fall den Verbindungsaufbbau abbrechen?
Ich versuchs nochmal anders 😃
Das Problem: z.B. Falsche Eingabe von ServerURL , oder Server TimeOut, dauert ewig und friert die GUI ein.
Ziel: Den Verbindungsaufbau in ein eigenen Thread packen, so dass man noch die möglichkeit hat "Abbrechen" zu klicken, wenn einem die Falscheingabe auffällt, bevor Server nicht gefunden oder sonstwas erst nach einiger Zeit kommt.
Danke.
Hi,
Du kannst mal SqlConnection.ConnectionTimeout prüfen.
Außerdem sollte es mit async/await und CancellationToken deutlich einfacher zu erstellen sein.
Hallo,
async/await und OpenAsync:
https://msdn.microsoft.com/de-de/library/hh223688%28v=vs.110%29.aspx
und hier noch ein Beispiel:
http://code.stonetip.com/2012/10/23/119/
Gruß
Steffen
Hallo,
danke für eure Antworten.
async/await.. quasi so?
private async void OKButton_Click(object sender, EventArgs e)
{
cts = new CancellationTokenSource();
try
{
await ProcessConnectSQLCancelleable(cts.Token);
}
catch (OperationCanceledException)
{
}
}
private async Task ProcessConnectSQLCancelleable(CancellationToken cancellationToken)
{
try
{
string connString = "Data Source=*****;Initial Catalog=***;Integrated Security=SSPI";
connection = new SqlConnection(connString);
await connection.OpenAsync(cancellationToken);
}
catch (SqlException sqlEx)
{
System.Diagnostics.Debug.WriteLine(sqlEx.Message);
}
}
Wenn ich richtig gelesen habe wäre es doof ein Async await nur für "den verbindungsaufbau" zu benutzen. Sollte ich dann auf den rest der Anwendung auch übertragen ?
Um z.B. später mehrere SQL Querys gleichzeitig ausführen zu können?
Ja, es macht keinen sinn, wenn nur (unnötige) Elemente asynchron sind. Jedenfalls auf das Gesamte gesehen.
async void ist auf alle Fälle ein NoGo, ein sogenannte pitfall.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Hi,
es wäre sinnvoll die anderen Methoden ebenfalls asynchron zu gestalten.
Du hast jetzt möglicherweise das Problem dass Exceptions nicht richtig gefangen werden. Gib mal einen fehlerhaften ConnectionString an und schaue ob die Exception gemeldet wird oder "unobserved" verläuft. Async void für einen Ereignishandler ist korrekt da hier keine Task-Signatur akzeptiert wird.
Du wirst wahrscheinlich eine vollständige Fehlerbehandlung in dem Click-Handler durchführen müssen, es können ja auch noch andere Fehler entstehen. Vllt kannst Du das in einem AsyncDelegateCommand o.ä. abstrahieren.
Hi,
danke für eure Antworten.
Dann werde ich mal versuchen meine Anwendung Asynchron aufzubauen ...
Gut das ich kein Architekt bin, das Haus wäre schon 20 mal wieder abgerissen worden 😁
Auf Asynchron umbauen, impliziert dann auch die darunter liegende Schichten? Also Server/Service muss auch Asynchrone Methoden bereitstellen?
Werde mich erst noch einlesen, da ich mit asynchron noch gar nichts gemacht habe. Dann finde ich bestimmt noch einige Stolperfallen.
Sollte ich das .net 4.5 verwenden ?
Ich weiß nicht, wie der Service aussieht zu dem du dich verbinden möchtest. Aber sofern es von der Schnittstelle her nix zum Abbrechen gibt wird da auch nichts abgebrochen werden.
Hi,
okay, also wenn die Service Schnittstelle nichts bereitstellt zum Abbrechen, kann nicht abgebrochen werden, und der Prozess läuft durch. Ich kann anstellen was ich möchte, sobald der "Verbindungsbefehl" abgesetzt wurde, gibt es kein zurück...
Nun habe ich mal versucht das ganze zu "verpacken" :
private void OKButton_Click(object sender, EventArgs e)
{
ConnectToServiceAsync();
}
private void CancelButton_Click(object sender, EventArgs e)
{
if (cts != null)
{
System.Diagnostics.Debug.WriteLine("Cancel Button Clicked";);
cts.Cancel();
}
}
private async void ConnectToServiceAsync()
{
cts = new CancellationTokenSource();
var cancellationToken = cts.Token;
var someTask = Task.Run(() =>
{
System.Diagnostics.Debug.WriteLine("SomeTask is Running...";);
System.Threading.Thread.Sleep(2000);
if (cancellationToken.IsCancellationRequested)
{
System.Diagnostics.Debug.WriteLine("cancellationToken.IsCancellationRequested";);
}
System.Diagnostics.Debug.WriteLine("Before Call Connect ...";);
ServiceAccess.ConnectToServerAsync(cancellationToken);
System.Diagnostics.Debug.WriteLine("After Call Connect ...";);
}, cancellationToken);
try
{
await someTask;
System.Diagnostics.Debug.WriteLine("try await someTask";);
}
catch(OperationCanceledException ex)
{
System.Diagnostics.Debug.WriteLine("OperationCanceledException ex";);
}
finally
{
System.Diagnostics.Debug.WriteLine("finally";);
}
}
ServiceAccess ConectToServerAsync(Cancellationtoken token)
{
public IServiceAsync ConnectToZyanAsync(CancellationToken token)
{
if (token.IsCancellationRequested)
{
System.Diagnostics.Debug.WriteLine("Cancellation observed in Service Access.";);
return null;
}
zyanConn = new ZyanConnection(zyanServiceUrl, protocol, _credentials, false, true);
.... .... ....
return _iServiceAsync;
}
Wenn ich nun OK Klicke, und innerhalb von 2000 ms "Abbrechen" klicke habe ich folgende Ausgabe:
SomeTask is Running...
Cancel Button Clicked
cancellationToken.IsCancellationRequested
Before Call Connect ...
Cancellation observed in Service Access.
Operation was canceled as expected.
After Call Connect ...
try await someTask
finally
Nehme ich den Thread.Sleep raus und klicke OK, und dann abbrechen erhalte ich folgende Ausgabe:
SomeTask is Running...
Before Call Connect ...
Cancel Button Clicked
finally
Wenn beim erreichen von "Before Call Connect" kein Cancel gedrückt wurde , läuft er durch und gibt kein zurück mehr, drücke ich vorher Cancel, wird der Verbindungsbefehl nicht abgesetzt.
Deswegen die Sleep Gedenkpause, merke ich "ooops, habe falsche url eingetippt, und drücke abbrechen" kann ich noch korrigieren, ansonsten muss ich auf Exception "Server nicht gefunden warten"
Kann man das so machen?
Danke
EDIT: habs nun so gelöst, denke das taugt für meinen Zweck :
5 Sekunden Gedenkzeit
Wird innerhalb von 5 Sek Abbrechen Button gedrückt abgebrochen, wird der Verbindungsaufbau nicht aufgerufen, der Status ändert sich in " Vorgang wird abgebrochen" (muss ja noch die 5 Sek abwarten, nach 5 Sekunden dann StatusText: Vorgang abgebrochen
Wird kein Abbrechen gedrückt, kommt irgendwann SocketException "Es konnte keine Verbindung hergestellt werden, da der Zielcomputer die Verbindung verweigerte"
Wird Abbrechen gedrückt nachdem die 5 Sek. Denkpause abgelaufen sind und der Verbindungsbefehl bereits abgesetzt wurde, kommt trotz Abbrechen die Socketexception. Die lasse ich dann einfach unterm Tisch fallen... weil ohne Verbindung soll eh nichts passieren, und wenn der User Abbrechen klickt, wird er wohl auch nichts mehr erwarten 😁 👍
Das mit der Wartezeit ist doch grober Unfug.
Starte einfach die Anfrage und gut ist. Wenn die Anfrage läuft und der Benutzer haut auf Abbrechen, dann signalisiere das über das CancellationToken. Kommt die Anfrage zu einem Ergebnis dann wird einfach das CancellationToken geprüft und bei "Oh, abgebrochen" einfach nichts mehr machen.
Gleichzeitig kann aber auch wieder sofort eine neue Anfrage abgesetzt werden, die sich genauso verhält.
Wenn du Benutzer erwartest, die auf den Anmelden/Abbruch-Buttons Steptanz machen, dann führe einfach einen Zähler ein, der bei jeder Anfrage hochgezählt wird und wenn die durchgelaufen ist, wieder heruntergezählt.
Wenn mehr als X Abfragen noch vakant sind, dann brems den Benutzer einfach aus "Ey, Langeweile ... ich zeige dir mal einen kurzweiligen Film"
Das mit der Wartezeit ist doch grober Unfug.
Starte einfach die Anfrage und gut ist. Wenn die Anfrage läuft und der Benutzer haut auf Abbrechen, dann signalisiere das über das CancellationToken. Kommt die Anfrage zu einem Ergebnis dann wird einfach das CancellationToken geprüft und bei "Oh, abgebrochen" einfach nichts mehr machen.
Muss ich nochmal drüber nachdenken, aber klingt mal ganz danach als würde ich es wieder viel komplizierter machen als es muss...
Hab das jetzt so umgesetzt.
Wenn ButtoNCancel gedrückt, abbrechen, und beim Catchen der Exception
catch (Exception ex)
{
if (cancellationToken.IsCancellationRequested)
{
//Do Nothing
} else
infotext.text = ex.Message;
}
ich geh einfach mal nicht von Steptanz auf den Buttons aus, denn so könnte es passieren dass im moment der exception gerade kein cancellationtoken vorhanden ist... falls nun doch die Exception durchrutscht und im Infofenster landet, ist es auch nicht weiter Schlimm...
und eigentliches Ziel war dass die UI nicht einfriert beim Verbindungsversuch (hatte ich schon fast wieder vergessen 😉 )
Das habe ich nun durch Async/Await gelöst.
var someTasks = Task.Run(() =>
{
ConnectToServer(cancellationToken, userNameTextBox.Text, passwordTextBox.Text);
CheckSqlConnectionExists();
}, cancellationToken);
try
{
await someTasks;
LoginDLg = DialogResult.OK;
this.Close();
}
Danke für eure Antworten.
Korrekt(er) wäre die Abfrage
// Were we already canceled?
token.ThrowIfCancellationRequested();
Dahinter steckt
if (token.IsCancellationRequested)
{
throw new OperationCanceledException(token);
}
Du vergisst den Token hier, was wiederum Seiteneffekte haben kann; je nachdem wie sauber Du ihn durch reichst und Du das eigentlich geplante Ziel hinter diesem Konstrukt folgst.
Dahingehend solltest Du auch im Catch OperationCanceledException
fangen.
Siehe auch Grundlagen dazu: Task Cancellation
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Hallo Abt,
brringt es mir denn etwas Cancellation bzw. OperationCanceledException abzufangen, wenn der abgesetzte Befehl kein "Cancelln" unterstützt?
OperationCanceledException würde (wird) auch erst geschmissen nachdem die eigentliche Exception kam, klar, catche ich die vor der allgemeinen Exception, bekomme ich OperationCancelled. Aber so oder so kann ich den "Verbindungsbefehl" nicht abbrechen, weil der Zyan Server das glaube ich nicht unterstützt. (Also so als wenn man mit dem Auto ohne Bremsen losrollen würde, und entweder rollt man aus, oder rollt gegen ein Baum, oder solange bis was passiert)
Ich denke, es hätte auch deine erste Lösung funktioniert, wenn du nicht im Cancel Event einen neue Instanz des Backgroundworkers erzeugt hättest sondern die vorher im Ok Event erzeugte verwendet hättest.
Also mit dem BackgroundWorker würde ich das wie folgt machen:
public partial class Form1 : Form
{
private BackgroundWorker _logonWorker;
private int _workerCount = 0;
private int _counter = 0;
public Form1()
{
InitializeComponent();
}
private void loginButton_Click( object sender, EventArgs e )
{
// alten Worker abbrechen
if ( _logonWorker != null && _logonWorker.IsBusy )
{
_logonWorker.CancelAsync();
}
if (_workerCount>10)
{
throw new Exception( "Too much workers. Please calm down!" );
}
_counter++;
label1.Text = String.Format( "login {0}", _counter );
_logonWorker = new BackgroundWorker();
_logonWorker.DoWork += new DoWorkEventHandler( logonWorker_DoWork );
_logonWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler( logonWorker_RunWorkerCompleted );
_logonWorker.WorkerSupportsCancellation = true;
_logonWorker.RunWorkerAsync( _counter );
_workerCount++;
}
private void logonWorker_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e )
{
_workerCount--;
if ( e.Cancelled )
{
// wir machen nichts
return;
}
else
{
if ( e.Error != null )
{
label1.Text = String.Format( "logon error {0}", e.Error.Message );
}
else
{
label1.Text = String.Format( "logon success {0}", e.Result );
}
}
}
private void logonWorker_DoWork( object sender, DoWorkEventArgs e )
{
try
{
// Login durchführen
Thread.Sleep( 3000 );
e.Result = e.Argument;
}
finally
{
if ( (sender as BackgroundWorker).CancellationPending )
e.Cancel = true;
}
}
private void cancelButton_Click( object sender, EventArgs e )
{
if ( _logonWorker != null && _logonWorker.IsBusy )
{
_logonWorker.CancelAsync();
label1.Text = "cancelled";
}
}
}