Solusi untuk C # 5 async re-entrancy

16

Jadi, ada sesuatu yang menggangguku tentang dukungan async baru di C # 5:

Pengguna menekan tombol yang memulai operasi async. Panggilan segera kembali dan pompa pesan mulai berjalan lagi - itulah intinya.

Jadi pengguna dapat menekan tombol lagi - menyebabkan masuknya kembali. Bagaimana jika ini masalah?

Dalam demo yang saya lihat, mereka menonaktifkan tombol sebelum awaitpanggilan dan mengaktifkannya lagi setelahnya. Bagi saya ini sepertinya solusi yang sangat rapuh dalam aplikasi dunia nyata.

Haruskah kita membuat kode semacam mesin negara yang menentukan kontrol mana yang harus dinonaktifkan untuk serangkaian operasi yang dijalankan? Atau ada cara yang lebih baik?

Saya tergoda untuk hanya menampilkan dialog modal selama operasi, tetapi ini terasa seperti menggunakan palu godam.

Adakah yang punya ide cemerlang?


EDIT:

Saya pikir menonaktifkan kontrol yang tidak boleh digunakan saat operasi sedang berjalan rapuh karena saya pikir itu akan cepat menjadi kompleks ketika Anda memiliki jendela dengan banyak kontrol di atasnya. Saya suka menjaga hal-hal sederhana karena mengurangi kemungkinan bug, baik selama pengkodean awal dan pemeliharaan selanjutnya.

Bagaimana jika ada kumpulan kontrol yang harus dinonaktifkan untuk operasi tertentu? Dan bagaimana jika beberapa operasi berjalan secara bersamaan?

Nicholas Butler
sumber
Apa yang rapuh tentang itu? Ini memberi pengguna indikasi ada sesuatu yang sedang diproses. Tambahkan beberapa pekerjaan indikasi dinamis yang sedang diproses dan sepertinya UI yang sangat baik bagi saya.
Steven Jeuris
PS: Apakah .NET 4.5's C # disebut C # 5?
Steven Jeuris
1
Hanya ingin tahu mengapa pertanyaan ini ada di sini dan bukan SO? Ini jelas tentang kode sehingga akan menjadi milik saya, saya kira ...
C. Ross
@ C.Ross, Ini lebih merupakan pertanyaan desain / UI. Sebenarnya tidak ada poin teknis yang perlu dijelaskan di sini.
Morgan Herlocker
Kami memiliki masalah serupa dalam bentuk HTML ajax di mana pengguna menekan tombol kirim, permintaan ajax dikirim dan kami kemudian ingin memblokir pengguna dari memukul kirim lagi, menonaktifkan tombol adalah solusi yang sangat umum dalam situasi itu
Raynos

Jawaban:

14

Jadi, ada sesuatu yang menggangguku tentang dukungan async baru di C # 5: Pengguna menekan tombol yang memulai operasi async. Panggilan segera kembali dan pompa pesan mulai berjalan lagi - itulah intinya. Jadi pengguna dapat menekan tombol lagi - menyebabkan masuknya kembali. Bagaimana jika ini masalah?

Mari kita mulai dengan mencatat bahwa ini sudah menjadi masalah, bahkan tanpa dukungan async dalam bahasa tersebut. Banyak hal yang dapat menyebabkan pesan terhapus saat Anda menangani suatu acara. Anda sudah harus kode membela diri untuk situasi ini; fitur bahasa baru hanya membuatnya lebih jelas , yaitu kebaikan.

Sumber paling umum dari loop pesan yang berjalan selama acara yang menyebabkan pemanggilan kembali yang tidak diinginkan adalah DoEvents. Hal yang menyenangkan tentang awaitsebagai lawan DoEventsadalah yang awaitmembuatnya lebih mungkin bahwa tugas-tugas baru tidak akan "kelaparan" tugas yang sedang berjalan. DoEventsmemompa loop pesan dan kemudian secara sinkron memanggil pengendali event re-entrant, sedangkan awaitbiasanya enqueues tugas baru sehingga akan berjalan di beberapa titik di masa depan.

Dalam demo yang saya lihat, mereka menonaktifkan tombol sebelum panggilan tunggu dan mengaktifkannya lagi sesudahnya. Saya pikir menonaktifkan kontrol yang tidak boleh digunakan saat operasi sedang berjalan rapuh karena saya pikir itu akan cepat menjadi kompleks ketika Anda memiliki jendela dengan banyak kontrol di atasnya. Bagaimana jika ada kumpulan kontrol yang harus dinonaktifkan untuk operasi tertentu? Dan bagaimana jika beberapa operasi berjalan secara bersamaan?

Anda dapat mengelola kompleksitas itu dengan cara yang sama seperti Anda mengelola kompleksitas lain dalam bahasa OO: Anda mengabstraksi mekanisme menjadi kelas, dan membuat kelas bertanggung jawab untuk menerapkan kebijakan dengan benar . (Cobalah untuk menjaga mekanisme secara logis berbeda dari kebijakan; harus mungkin untuk mengubah kebijakan tanpa terlalu banyak mempermainkan mekanisme.)

Jika Anda memiliki formulir dengan banyak kontrol di atasnya yang berinteraksi dengan cara yang menarik, maka Anda hidup dalam dunia yang rumit dari buatan Anda sendiri . Anda harus menulis kode untuk mengelola kerumitan itu; jika Anda tidak menyukainya maka sederhanakan formulirnya sehingga tidak terlalu rumit.

Saya tergoda untuk hanya menampilkan dialog modal selama operasi, tetapi ini terasa seperti menggunakan palu godam.

Pengguna akan membencinya.

Eric Lippert
sumber
Terima kasih Eric, saya kira ini adalah jawaban yang pasti - terserah pengembang aplikasi.
Nicholas Butler
6

Mengapa Anda berpikir menonaktifkan tombol sebelum awaitdan kemudian mengaktifkannya kembali ketika panggilan selesai rapuh? Apakah karena Anda khawatir tombol itu tidak akan pernah diaktifkan kembali?

Nah, jika panggilan tidak kembali maka Anda tidak dapat membuat panggilan lagi, jadi sepertinya ini adalah perilaku yang Anda inginkan. Ini menunjukkan status kesalahan yang harus Anda perbaiki. Jika panggilan async Anda habis maka ini masih bisa meninggalkan aplikasi Anda dalam keadaan tak tentu - lagi yang memiliki tombol diaktifkan bisa berbahaya.

Satu-satunya saat ini mungkin menjadi berantakan adalah jika ada beberapa operasi yang mempengaruhi keadaan tombol, tapi saya pikir situasi itu harus sangat jarang.

ChrisF
sumber
Tentu saja Anda harus menangani kesalahan - Saya telah memperbarui pertanyaan saya
Nicholas Butler
5

Saya tidak yakin apakah ini berlaku untuk Anda karena saya biasanya menggunakan WPF, tapi saya melihat Anda mereferensikan ViewModeljadi mungkin.

Alih-alih menonaktifkan tombol, atur IsLoadingbendera ke true. Kemudian setiap elemen UI yang ingin Anda nonaktifkan dapat diikat ke flag. Hasil akhirnya adalah menjadi tugas UI untuk memilah status diaktifkan sendiri, bukan Logika Bisnis Anda.

Selain itu, sebagian besar tombol di WPF terikat ke ICommand, dan biasanya saya mengatur ICommand.CanExecutesama dengan !IsLoading, sehingga secara otomatis mencegah perintah dari mengeksekusi ketika IsLoading sama dengan true (juga menonaktifkan tombol untuk saya)

Rachel
sumber
3

Dalam demo yang saya lihat, mereka menonaktifkan tombol sebelum panggilan tunggu dan mengaktifkannya lagi sesudahnya. Bagi saya ini sepertinya solusi yang sangat rapuh dalam aplikasi dunia nyata.

Tidak ada yang rapuh tentang ini, dan itulah yang diharapkan pengguna. Jika pengguna perlu menunggu sebelum menekan tombol lagi maka buat mereka menunggu.

Saya dapat melihat bagaimana skenario kontrol tertentu dapat memutuskan kontrol mana yang akan dinonaktifkan / diaktifkan pada satu waktu yang cukup kompleks, tetapi apakah mengejutkan bahwa mengelola UI yang rumit akan menjadi rumit? Jika Anda memiliki terlalu banyak efek samping yang menakutkan dari kontrol tertentu, Anda selalu dapat menonaktifkan seluruh formulir dan melemparkan "memuat" whirly-manggung atas semuanya. Jika ini adalah kasus pada setiap kontrol tunggal, maka UI Anda mungkin tidak dirancang dengan sangat baik di tempat pertama (kopling ketat, pengelompokan kontrol yang buruk, dll.).

Morgan Herlocker
sumber
Terima kasih - tetapi bagaimana mengelola kompleksitasnya?
Nicholas Butler
Salah satu opsi adalah menempatkan kontrol terkait ke dalam wadah. Nonaktifkan / aktifkan dengan wadah kontrol, bukan oleh kontrol. yaitu: EditorControls.Foreach (c => c.Enabled = false);
Morgan Herlocker
2

Menonaktifkan widget adalah opsi yang paling kompleks dan rawan kesalahan. Anda menonaktifkannya di handler sebelum memulai operasi async dan Anda mengaktifkannya kembali di akhir operasi. Jadi, disable + enable untuk setiap kontrol disimpan lokal untuk menangani kontrol itu.

Anda bahkan dapat membuat pembungkus, yang akan mengambil delegasi dan kontrol (atau lebih dari mereka), menonaktifkan kontrol dan menjalankan sesuatu secara tidak sinkron, yang akan melakukan delegasi dan daripada mengaktifkan kembali kontrol. Itu harus menangani pengecualian (lakukan mengaktifkan akhirnya), sehingga masih berfungsi dengan andal jika operasi gagal. Dan Anda dapat menggabungkannya dengan menambahkan pesan di bilah status, sehingga pengguna tahu apa yang dia tunggu.

Anda mungkin ingin menambahkan beberapa logika untuk menghitung penonaktifan, sehingga sistem tetap berfungsi jika Anda memiliki dua operasi, yang harus menonaktifkan operasi yang ketiga, tetapi tidak saling terpisah. Anda menonaktifkan kontrol dua kali, sehingga akan diaktifkan kembali hanya jika Anda mengaktifkannya dua kali lagi. Itu harus mencakup sebagian besar kasus sambil menjaga kasus terpisah sebanyak yang Anda bisa.

Di sisi lain apapun seperti mesin negara akan menjadi kompleks. Dalam pengalaman saya, semua mesin negara yang pernah saya lihat sulit untuk dirawat dan mesin negara Anda harus mencakup seluruh dialog, mengikat segalanya untuk semuanya.

Jan Hudec
sumber
Terima kasih - saya suka ide ini. Jadi, apakah Anda memiliki objek manajer untuk jendela Anda yang menghitung jumlah permintaan menonaktifkan & mengaktifkan untuk setiap kontrol?
Nicholas Butler
@NickButler: Ya, Anda sudah memiliki objek manajer untuk setiap jendela — formulir. Jadi saya memiliki kelas yang mengimplementasikan permintaan dan penghitungan dan hanya menambahkan contoh ke formulir yang relevan.
Jan Hudec
1

Meskipun Jan Hudec dan Rachel telah mengungkapkan ide-ide terkait, saya akan menyarankan menggunakan sesuatu seperti arsitektur Model-View - ungkapkan keadaan bentuk dalam suatu objek dan ikat elemen-elemen bentuk ke keadaan itu. Ini akan menonaktifkan tombol dengan cara yang dapat menskala untuk skenario yang lebih kompleks.

psr
sumber