Dostlar, kafayı yiyecektim. Şu an üzerinde uğraştığım masaüstü uygulamasında (WinForms, tabii ki) uzun süren bir veri işleme döngüsü var. Kullanıcı "İşlemi Başlat"a basıyor, benim kod arka planda koca bir List<T>'yi dönüp duruyor ve arayüz tamamen donuyor. "İptal" butonu bile tıklanmıyor, mouse işareti saat simgesine dönüyor. Klasik.
StackOverflow'da bile "UI responsive tutmak" diye aratınca, ilk çıkan cevaplardan biri, o meşhur ve tehlikeli yöntem: Application.DoEvents(). "Şuraya bir tane koyayım, döngüde her adımda UI'ın nefes almasını sağlar" diye düşündüm. Ve evet, çalıştı!
C#:
for (int i = 0; i < devListe.Count; i++)
{
// Uzun süren işlem...
VeriyiIsle(devListe[i]);
// İlkel çözüm! UI'ı tazele.
Application.DoEvents();
}
Arayüz artık donmuyordu, progress bar ilerliyordu, hatta iptal butonuna basılabiliyordu. Mutluluk.
Ta ki, kullanıcının işlem devam ederken arayüzdeki başka bir butona hızlıca tıklayabileceği aklıma gelene kadar. Şaka gibi ama, DoEvents() o tıklama olayını da işliyor! Yani, uzun süren işlemin ortasında, başka bir event handler'ı tetiklenebiliyor. Bu da, paylaşılan kaynaklara (aynı listeye, aynı değişkene) aynı anda erişim denemesi demek.
Sonuç? NullReferenceException, garip davranışlar, bazen de tamamen çöküş. Debug ederken olayların sırasını takip etmek imkansız hale geldi. Kod, re-entrancy (tekrar giriş) denen canavara dönüştü. Sanki tek bir iş parçacığında iki farklı kod aynı anda koşmaya çalışıyor gibiydi.
Neyse ki, eski günahlarımdan döndüm. WinForms dünyasında bunun için güvenli yollar var. Eski dostum BackgroundWorker veya daha modern yaklaşım Task.Run() ile async/await.
İşte basit bir Task.Run() örneği:
C#:
private async void btnIslemiBaslat_Click(object sender, EventArgs e)
{
btnIslemiBaslat.Enabled = false;
progressBar1.Visible = true;
await Task.Run(() =>
{
for (int i = 0; i < devListe.Count; i++)
{
// Uzun süren işlem UI thread'ini bloklamaz!
VeriyiIsle(devListe[i]);
// Progress bar'ı güncellemek için Invoke kullan.
progressBar1.Invoke(new Action(() => { progressBar1.Value = (i 100) / devListe.Count; }));
}
});
// İşlem bitti, UI'ı eski haline getir.
progressBar1.Visible = false;
btnIslemiBaslat.Enabled = true;
}
Bu yöntemde, uzun işlem arka planda bir thread'de koşarken, UI thread'i (ana thread) serbest kalıyor ve kullanıcı etkileşimlerine cevap verebiliyor. İptal butonu için CancellationToken kullanmak da mümkün.
Sonuç olarak, DoEvents() hızlı bir fix gibi görünse de, aslında koduna istemediğin karmaşıklıklar ve bug'lar ekleyen bir tuzak. Ben bu tuzağa düştüm, siz düşmeyin. Acil durumlarda bile kullanmadan önce iki kere düşünün.
Peki ya siz? Hiç DoEvents() kullanıp sonradan başınızı duvarlara vurduğunuz oldu mu? Ya da WinForms/WPF'te UI'ı responsive tutmak için favori, temiz yönteminiz nedir?