Laden...

[Gelöst] Server/Datenbank Verbindung in Thread Herstellen und behandeln

Erstellt von elTorito vor 8 Jahren Letzter Beitrag vor 8 Jahren 3.772 Views
elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren
[Gelöst] Server/Datenbank Verbindung in Thread Herstellen und behandeln

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

2.298 Beiträge seit 2010
vor 8 Jahren

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 |

elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren

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?

elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren

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.

W
955 Beiträge seit 2010
vor 8 Jahren

Hi,

Du kannst mal SqlConnection.ConnectionTimeout prüfen.
Außerdem sollte es mit async/await und CancellationToken deutlich einfacher zu erstellen sein.

S
322 Beiträge seit 2007
vor 8 Jahren

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

elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren

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?

16.832 Beiträge seit 2008
vor 8 Jahren

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.

W
955 Beiträge seit 2010
vor 8 Jahren

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.

elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren

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 ?

elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren

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

  1. 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

  2. Wird kein Abbrechen gedrückt, kommt irgendwann SocketException "Es konnte keine Verbindung hergestellt werden, da der Zielcomputer die Verbindung verweigerte"

  3. 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 😁 👍

D
985 Beiträge seit 2014
vor 8 Jahren

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"

elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren

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...

elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren

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.

16.832 Beiträge seit 2008
vor 8 Jahren

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

elTorito Themenstarter:in
177 Beiträge seit 2009
vor 8 Jahren

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)

463 Beiträge seit 2009
vor 8 Jahren

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.

D
985 Beiträge seit 2014
vor 8 Jahren

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";
            }                

        }
    }