[ dusans @ 09.09.2017. 13:43 ] @
Počinjem rad na projektu (wpf client) koji će zahtevati gomilu
poziva ka web-api servisima pa se javlja potreba za "elegantnim"
i univerzalnim mehanizmom koji se tiče asinhronih operacija.

Za početak, mehanizam treba da omogući sledeće varijante:

1. Pokretanje nove operacije uz otkazivanje trenutno aktivne operacije
- ukoliko uopšte prethodna postoji, bez čekanja da se ona završi

2. Pokretanje nove operacije samo ako ne postoji trenutno aktivna

Pokušao sam da pronađem reentrancy rešenja online, međutim,
uglavnom se radi o varijantama koje mi ne trebaju ili se radi o
copy/paste rešenjima sa varijablama kakva sam već ranije koristio:
https://blogs.msdn.microsoft.c...-the-patterns-to-deal-with-it/

Pošto async/await može da bude prilično čupav za debugging,
pristalica sam gotovog kvalitetnog rešenja ali ga ne pronalazim :(

Ovo je moje rešenje ali nisam siguran što se tiče robustnosti:

Async operation coordinator:
Code (csharp):

public class AsyncOperation<T>
{
    // Current state
    private Task<T> task;
    private CancellationTokenSource cts;

    public Task<T> Run(Func<CancellationToken, Task<T>> action, bool cancelPrevious = true)
    {
        // Return current task if known and don't wish to cancel
        if (!cancelPrevious && task?.IsCompleted == false) return task;

        // Cancel current task
        if (task != null)
        {
            cts.Cancel();
            cts = null;
            task = null;
        }

        // Run new task
        cts = new CancellationTokenSource();
        task = action(cts.Token);
        return task;
    }
}
 


Use case:
Code (csharp):

// Operation coordinator instance
private AsyncOperation<int> asyncFoo = new AsyncOperation<int>();

// Button click handler
private async void buttonFoo_Click(object sender, EventArgs e)
{
    // Run/restart foo operation
    await asyncFoo.Run(ct => Foo(ct, 10, 20));
}

// Actual custom operation
private async Task<int> Foo(CancellationToken ct, int a, int b)
{
    try
    {
        // Call service here
        await Task.Delay(5000, ct);

        // Call service here
        await Task.Delay(5000, ct);
    }
    catch (TaskCanceledException)
    {
        return 0;
    }

    // Check for cancellation
    if (ct.IsCancellationRequested) return 0;

    // Process results here
    // ...

    return 1;
}
 

Da li postoji neko gotovo rešenje?
Šta bi bile zamerke odnosno saveti za improvement?
[ dusans @ 10.09.2017. 12:32 ] @
Anyone?
[ Branimir Maksimovic @ 10.09.2017. 12:44 ] @
Pa nisam C# programer niti mi je poznata biblioteka, ali rekao bih da ne znam sta znaci ono CancelPrevoius? Imas neku listu taskova pa znas koji je prethodni ili moze max 2 taska u queue?
[ dejanet @ 10.09.2017. 14:29 ] @
Nisam radio "fat client" preko 10 godina, wpf nikada, a i tada sam retko radio callback (tada nije postojao Async/Await api).

Ono sto bi u tvom slucaju proverio da li je "skuplja" operacija task cancelation ili da pustis da se izvrsi i eventualno uradi update nekog model object-a koji mozes da bindujes na form kasnije.

Drugo uradio bi analizu da li imas podatke koje mozes da drzis u cache-u, tako da ih ne dobijas iz web api-ija svaki put na UI action. Ako imas veci broj takvih podataka, onda ovaj pristup moze samo da ti iskomplikuje zivot.

Trece, za client ti ne treba robusno resenje, bitnije da vodis racuna da ne bombardujes server za zahtevima, tj. da ih svedes na najmanju mogucu meru.
U tu svrhu vaznije je da imas neki cache ili mozda singleton class gde ces da drzis neophodne podatke koje dobijas sa servera, npr. neki model object klasu, koju mozes da bindujes po potrebi na form.

Jbg. opet kazem nisam radio dugo neki fat desktop client, a u medjuvremenu se dosta toga promenilo. Mislim da ima par ljudi na forumu koji prate najnovija desavanja, framework-e, pristupe u kontekstu wpf aplikacije, ali se ne javljaju za sada
[ Branimir Maksimovic @ 10.09.2017. 15:13 ] @
Ne znam, samo znam da nije nasao dobar primer. Nervozni korisnik kad pocne da klikce po dugmetu obicno ocekuje da ubrza task ne da ga kanceluje ;)
Mozda mu i nije potrebna ta gimnastika sa kancelovanjem, ako se vec radi o dugmicima. Ako je poduza operacija moze prosto da doda dugme 'cancel' i ne razmislja vise o tome...
[ dejanet @ 10.09.2017. 15:19 ] @
^ Tu je pametnije raditi UI blocking, mada i tu treba obraditi scenario kada iz web api-ija dobije error ili je service nedostupan.
[ dusans @ 10.09.2017. 22:35 ] @
Hvala na diskusiji.
Pored tehničkih rešenja, više sam i zainteresovan da čujem iskustva-pristupe koji se tiču ponašanja GUI-ja u ovakvim situacijama,
šta se dobro pokazalo i za implementaciju i za korisnike, tako da sam uvek otvoren za korisne predloge i prepisivanje.
A i verujem da se dosta ljudi sreće sa ovakvim problemima u praksi.

Citat:
dejanet:
Trece, za client ti ne treba robusno resenje, bitnije da vodis racuna da ne bombardujes server za zahtevima, tj. da ih svedes na najmanju mogucu meru.

Citat:
dejanet:
^ Tu je pametnije raditi UI blocking, mada i tu treba obraditi scenario kada iz web api-ija dobije error ili je service nedostupan.


UI blocking je dobro rešenje za neke situacije, za neke postoje requirement-i da se GUI ne disable-uje.

Konkretan primer - lista sa filterima, page-ingom i sortiranjem ne bi trebala da se disable-uje ni na koji način
dok traje operacija učitavanja (ma koliko trajala).
Dakle, korisnik slobodno šeta, sortira, filteru-je, refresh-uje i efektivno ga zanima samo ono
što je poslednja operacija učitavanja dovukla - ono što je u pre toga u tom njegovom kliktanju
bilo inicirano jednostavno propada.

Drugi primer - master-detail view, korisnik slobodno šeta i selektuje master-a a u pozadini se
učitavaju podaci detalja bez da se bilo šta na master-u disable-uje dok učitavanje traje.

U principu, ako se koristi UI blocking onda ova diskusija manje-više postaje bespredmetna.

Što se tiče bombardovanja servera - apsolutno dobra poenta, nije mi palo na pamet da i to treba uzeti u obzir.
Možda bih trebao da uvedem nekakvu vrstu penalizacije tako da kada nervozni korisnik krene sa preteranim
kliktanjem, ja pravim sve veći i veći delay pre efektivnog poziva serveru ;)

Što se tiče cache-inga, samo neke stvari ako se pokaže potreba za time, nikako kao generalni pristup sve-kroz-keš.

[Ovu poruku je menjao dusans dana 11.09.2017. u 00:17 GMT+1]
[ negyxo @ 11.09.2017. 08:27 ] @
Prvo, ovo bi mogao da napises krace:

Code (csharp):

    public Task<T> Run(Func<CancellationToken, Task<T>> action, bool cancelPrevious = true)
    {
        // Return current task if known and don't wish to cancel
        if (!cancelPrevious && task?.IsCompleted == false) return task;

        cts?.Cancel();
        cts = new CancellationTokenSource();
        task = action(cts.Token);
        return task;
    }
 



Ako je sve sto zelis dalje da cancelujes nema potrebe da proveravas task i da nullujes, posto ce dodeljivanje nove reference poslati vec postojece u void ako nigde nisu referencirane :)


...ali, spomenuo si reentrant metode, sto ovde nije slucaj, imas task i cts promenljive i one nisu bezbedne za koriscenje, tj. ako ces sve da teras sa UI thread-a onda OK, ali vodi racuna sta tvoj API govori, kako komunicira sa potencijalnim korisnicima, u ovom slucaju ja bi rekao da bi trebalo da je sve thread safe, posto vidim task deklaraciju, pa bi me osecaj zavarao. Zbog toga, morao bi da dodas lock :(, ili da namestis da se tvoja klasa izvrsava uvek na jednom threadu (sto moze biti overkill za ovako malu funkcionalnost).

Sto se tice generalno saveta da li da blokiras UI ili ne, naravno da je odgovor da ne blokiras, ali onda asinhroni pristup zahteva vise napora kako bi se uradilo kvalitetno (recimo, ako korisnik stisne dugme, i ti nesto pocnes da radis, korisnik ocekuje feedback, ako recimo on ne dodje u roku od pola sekunde, onda mu izbacis mali progress pored dugmeta ili tako nesto... uglavnom dosta tweak-ovanja je potrebno za smooth UI).
[ dusans @ 11.09.2017. 09:15 ] @
Da - u primeru nisu thread safe i nema lock-ovanja da ne bih komplikovao suštinu.
Namera jeste da se jednoj instanci AsyncOperation uvek pristupa iz istog thread-a,
a na kraju i uvesti lock za multithread u AsyncOperation je trivijalna stvar.
Međutim, thread-safe implementacija u custom operacija koje uglavnom koriste
neki shared state je veća problematika, tako da za sada i ne razmišljam
da ulazim u takve priče bez neke preke potrebe.

Što se tiče feedback-a user-u, on postoji i implementiran je baš kao što si naveo,
posle određenog vremena se pojavljuje indikacija dešavanja u pozadini.
[ mmix @ 12.09.2017. 11:28 ] @
Ja realno u tim situacijama radim no operation pristup (multiple reentrancy samo izadje) ili GUI blocking. Ponekad, ali retko, radim semafor i queueing, ako hocu da korisnik moze sa vise klikova da zada vise uzastopnih zahteva.
Razlog je sto ne zelim da maltretiram servere, iako ti uradis Cancel task-a u pozadini zahtev i dalje traje, rabe se resursi servera i u krajnjoj liniji smanjuje propusnost.