[C#] Zakończenie wątku działającego w tle - backgroundworker


(Musialowski Tomasz) #1

Mam pewną funkcję, która sprawdza przy rejestracji czy wpisywany login nie jest już przypadkiem przez kogoś używany. Chciałem aby osoba rejestrująca się była w miarę na bieżąco z tą informacją dlatego funkcję tą wywołuję przy każdym TextChange a żeby nie blokować użytkownikowi pisania odpalam tą metodę w tle za pomocą BackgroundWorkera. Wszystko jest ok gdy funkcję wywołuję tak:

private void login_TextChanged(object sender, TextChangedEventArgs e)

{

if (!checkLogin.IsBusy)

    checkLogin.RunWorkerAsync(login.Text);

}

Może się zdarzyć jednak tak, że gdy baza będzie długo nie odpowiadać to dopisanie kolejnych znaków zostanie pominięte gdyż !checkLogin.IsBusy będzie zwracało false. Chciałem zakończyć więc wykonywanie wątku w tle za pomocą CancelAsync() w ten sposób:

private void login_TextChanged(object sender, TextChangedEventArgs e)

        {

            if (checkLogin.IsBusy)

                checkLogin.CancelAsync();

            checkLogin.RunWorkerAsync(login.Text);

        }

Czyli jeżeli metoda się wykonuje w tle to zakończ ją i odpal z nową wartością login.Text. Niestety kompilator zwraca błąd informujący, że Backgroundworker jest zajęty aktualnym zadaniem. WorkerSupportsCancellation mam ustawione na true. Zaczynają mi się kończyć pomysły a rozwiązania z internetu zdają mi się nic nie wnosić do problemu. Z góry dziękuję za pomoc.


(somekind) #2

Na pewno nie kompilator, bo gdyby on zwrócił jakiekolwiek błędy, to program by Ci się nie uruchomił.

Anulowanie pracy NIE jest natychmiastowe, o czym świadczy chociażby nazwa metody "CancelAsync", więc nie możesz bezpośrednio w następnej instrukcji uruchamiać Workera na nowo.

Myślę, że powinieneś obsłużyć zdarzenie "RunWorkerCompleted", w nim sprawdzać czy Worker został anulowany (przez właściwość "Cancelled"), a jeśli tak, to dopiero wtedy wywoływać ponownie "RunWorkerAsync".

Aha - przede wszystkim metoda "CancelAsync" nie przerywa pracy Workera, a jedynie ustawia właściwość "CancelationPending" na true. Zatem:

1) Wywołujesz "CancelAsync";

2) W obsłudze zdarzenia "DoWork" sprawdzasz, czy "CancelationPending" jest true;

3) Jeśli tak, to ustawiasz "e.Cancel" na true oraz przerywasz jakoś działanie kodu (np. break w pętli);

4) W "RunWorkerCompleted", jeśli "e.Cancelled" jest true, to ponownie uruchamiasz Workera.


(Musialowski Tomasz) #3

Wybacz, czasem coś z rozpędu napiszę :slight_smile: Rzecz jasna chodziło mi o debuger. OK, nie wiem czy dokładnie zrozumiałem co mam zrobić ale wydaje mi się, że tak. Jak coś to będę informował.

Edit:

Niestety nie zadziałało. Wciąż przy wystarczająco szybkim wpisaniu istniejącego loginu do TextBoxa nie nadąża z wykonaniem funkcji sprawdzającej i ignoruje ostatnio wpisane litery.


(somekind) #4

Pytanie, co nie zadziałało? Bo raczej nie anulowanie pracy Workera, o ile zrobiłeś je w przedstawiony sposób.

Myślę, że tutaj problemem może być raczej ogólna koncepcja założeń tej funkcjonalności. Powinieneś to zrealizować jakoś inaczej, tak aby metoda odpytująca bazę zawsze operowała na całym szukanym tekście.


(Musialowski Tomasz) #5

Próbowałem to zrobić też tak, że po każdym skończonym zadaniu, funkcja jest uruchamiana w tle jeszcze jeden raz. Niestety nie wykonuje się one w ogóle lub wpada w nieskończoną pętlę:

private void checkLogin_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

        {

            if (!checkLogin.CancellationPending) //zadanie nigdzie nie jest anulowane więc jest nieprawda, że false czyli true

            {

                checkLogin.RunWorkerAsync(login.Text); //tutaj odpalam jeszcze raz Workera

                checkLogin.CancelAsync(); //i ustawiam CancellationPeding na true by nie weszło do tego bloku przy skończonym zadaniu checkLogin_DoWork

            }

            //MessageBox.Show("Boże daj by to działało");

        }

Działa ale niestety mamy nieskończoną pętlę w tle.


(Tomek Matz) #6

Nie widzę, w którym momencie w procedurze checkLogin_RunWorkerCompleted zwracasz informację o tym, że login jest zajęty lub nie. W obrębie procedury checkLogin_DoWork nie możesz tego robić, a przynajmniej nie powinieneś tego robić, bo w obrębie tej procedury nie jest zalecane wykonywanie jakiejkolwiek interakcji z GUI.

To jest OK:

Kod, który umieściłeś aktualnie w procedurze checkLogin_RunWorkerCompleted nie ma za dużego sensu. Zrób to tak, że dodaj sobie dodatkową zmienną do form-y (lub zamiast tego możesz ustawiać właściwość Result klasy DoWorkEventArgs). Ona będzie przechowywać login, który jest aktualnie sprawdzany. W procedurze checkLogin_RunWorkerCompleted będziesz sprawdzać, czy aktualny login przechowywany w textbox-ie jest taki sam jak ten, który miałeś w tej swojej zmiennej (lub właściwości Result klasy RunWorkerCompletedEventArgs). Jeśli jest taki sam to oznacza, że nie było żadnych opóźnień (mało prawdopodobne) i w związku z tym możesz zwrócić informację o tym, czy login jest zajęty, czy też nie. Jeśli jest inny to znaczy, że były jakieś opóźnienia w trakcie sprawdzania login-u i użytkownik zdążył już podopisywać kolejne litery, i w związku z tym jeszcze raz musisz uruchomić sprawdzanie (checkLogin.RunWorkerAsync(login.Text)). Poza tym, żeby użytkownik widział, że praca jest w toku musisz dodać jakąś klepsydrę. Tak więc, gdy wystąpią jakieś opóźnienia będzie wiedział, że coś się dzieje.

Żeby zredukować ilość zapytań do bazy danych mógłbyś też pomyśleć, czy da radę cache-ować perspektywę z loginami. Wówczas mógłbyś synchronicznie sprawdzać, czy login jest zajęty, czy też nie, przy czym musiałbyś zadbać o to, aby co jakiś czas odświeżać ten cały cache.


(Musialowski Tomasz) #7

Zrobiłem nie do końca tak jak sugerowałeś ale naprowadziłeś mnie razem z somekind'em na prawidłowe rozwiązanie za co jestem bardzo wdzięczny. W funkcji DoWork nic nie zmieniam w GUI. Robię to w ProgressChanged. Z DoWork przekazuję tam zmienną z komunikatem (lub jego brakiem) i tam ustawiany jest odpowiedni TextBlock.

Dodałem do klasy pole oldLogin, do którego w DoWork zapisywany jest aktualnie przetwarzany login. Potem tylko mała zmiana w Completed:

private void checkLogin_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

        {

            if (oldLogin != login.Text)

            {

                checkLogin.RunWorkerAsync(login.Text);

            }

        }

Jeszcze raz dzięki :slight_smile:


(Tomek Matz) #8

Procedurę checkLogin_DoWork powinieneś traktować tak jakby była oznaczona słowem kluczowym static. W jej obrębie nie zmieniaj wartości oldLogin. Zrób to przed wywołaniem tej procedury lub (lepiej) przekazuj poprzez właściwości Result EventArgs-ów do procedury checkLogin_RunWorkerCompleted. Zawsze jedna zmienna w klasie mniej.


(Musialowski Tomasz) #9

Ok, dodałem to więc przed wywołaniem żeby nie przekombinować bo nie wiem czy tą drugą metodą bym nie namieszał :slight_smile:

private void login_TextChanged(object sender, TextChangedEventArgs e)

        {

            if (!checkLogin.IsBusy)

            {

                oldLogin = login.Text;

                checkLogin.RunWorkerAsync(login.Text);

            }

        }

(Tomek Matz) #10

W porządku, może być. Dorzuć jeszcze tylko, żeby wyświetlała się użytkownikowi informacja o tym, że sprawdzanie dostępności loginu jest w toku (czy coś w tym stylu) i gotowe.