Non-javascript fallbacks in ASP.NET MVC nl

Door creator1988 op woensdag 5 januari 2011 13:42 - Reacties (13)
Categorie: Frontend, Views: 3.933

Alles op den ganse aarde is tegenwoordig Ajaxified, en een behoorlijk aantal websites heeft haar Ajax interacties Javascript-only gemaakt. Nogal vervelend als een aanzienlijk deel van je gebruikers onder werktijd je website probeert te gebruiken. Met ASP.NET MVC is het behoorlijk gemakkelijk gemaakt om eenvoudige interacties ook beschikbaar te maken voor non-Javascript clients.

De 'Bewaren' knop
Men neme als voorbeeld, de 'Bewaren als favoriet' functionaliteit zoals deze op elke detailpagina te zien is. Een knop met drie states:
http://www.100procentjan.nl/tweakers/nonjs_bewaren.png
http://www.100procentjan.nl/tweakers/nonjs_bewaarprogress.png
http://www.100procentjan.nl/tweakers/nonjs_bewaard.png

Aan de achterkant doet dit niets anders dan een GET naar de controller 'ClientActie' (GETs zijn goedkoper dan POSTs) via:

JavaScript:
1
$.get('/clientactie/bewaarobject/?id=47883154', function(d) { /* change state */ }, 'json');



En nu zonder Javascript?!
Allereerst is het zaak om altijd een werkende link te hebben achter een knop. Wanneer je 'Telefoon' linkje normaal direct het telefoonnummer toont, zorg er dan voor dat de <a href/> naar je contact-pagina wijst. Zo breek je je interactie niet. Voor het 'Bewaren' linkje hebben we de volgende link:

HTML:
1
2
3
<a href="?clientactie=bewaarobject" onclick="...">
    <strong><span class="icn-favor">Bewaren als favoriet</span></strong>
</a>


Als de gebruiker nu klikt, wordt hij doorgestuurd naar '/huidigepagina/?clientactie=bewaarobject'. We moeten dit nu enkel afvangen in de controller die deze pagina serveert.

Action filter
In ASP.NET MVC kan je 'action filters' gebruiken om via attributes op je actions generiek gedrag te implementeren. Een filter voor de 'clientactie' behavior ziet er ongeveer zo uit:

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ClientActieHandler : ActionFilterAttribute {
    public override void OnActionExecuting(FilterExecutingContext context) {
        // haal huidige actie uit de context
        var clientactie = context.HttpContext.Request["clientactie"];
        
        if(!string.IsNullOrEmpty(clientactie))
        {
            ClientActieController clientActieController = new ClientActieController();
            
            switch(clientactie)
            {
                case "bewaarobject":
                    // haal huidig ID uit de context
                    // ja dit is niet echt mooi; maar soit
                    var id = int.Parse(context.RouteData.Values["id"].ToString());
                    clientActieController.BewaarObject(id);
                    break;
            }
        }
    }
}


Nu kan je je Action decoraten met het attribute:

C#:
1
2
3
4
5
6
7
8
public class DetailController : Controller {
    // dit is de normale detail controller die de detailpagina's rendert
    
    [ClientActieHandler]
    public ActionResult Overzicht() {
        /* inner working */
    }
}


Et voila! Bij elke aanroep wordt bekeken of er nog clientacties zijn die verwerkt moeten worden.

View
In de view hoeven we geen wijzigingen te maken omdat er hier geen verschillen zijn tussen de Javascript en de non-Javascriptgebruiker (pseudo-code):

ASP.NET:
1
2
3
4
5
<% if(Profile.IsStoredObject(Model.Id)) { %>
        <!-- render 'bewaard' knop -->
    <% } else { %>
        <!-- render 'bewaren' knop -->
    <% } %>

Volgende: MVC Views gebruiken in ASP.NET Webforms 01-'11 MVC Views gebruiken in ASP.NET Webforms
Volgende: 2010: Nu eens échte cijfers over browsers en besturingssystemen 01-'11 2010: Nu eens échte cijfers over browsers en besturingssystemen

Reacties


Door Tweakers user CodeCaster, woensdag 5 januari 2011 14:03

Wat bedoel je met
GETs zijn goedkoper dan POSTs
? In de zin van verstuurde bytes?

code:
1
2
3
4
5
POST /index.html HTTP/1.1
Content-Length: 8
Cache-Control: no-cache

test=123


Versus:

code:
1
GET /index.html?test=123 HTTP/1.1


Of zit er nog meer achter? :)

Door Tweakers user creator1988, woensdag 5 januari 2011 14:07

CodeCaster schreef op woensdag 05 januari 2011 @ 14:03:
? In de zin van verstuurde bytes?
Of zit er nog meer achter? :)
POST verzend twee pakketten, en GET maar een. Zie deze pagina van Yahoo:
The Yahoo! Mail team found that when using XMLHttpRequest, POST is implemented in the browsers as a two-step process: sending the headers first, then sending data. So it's best to use GET, which only takes one TCP packet to send (unless you have a lot of cookies). The maximum URL length in IE is 2K, so if you send more than 2K data you might not be able to use GET.
An interesting side affect is that POST without actually posting any data behaves like GET. Based on the HTTP specs, GET is meant for retrieving information, so it makes sense (semantically) to use GET when you're only requesting data, as opposed to sending data to be stored server-side.

Door Tweakers user skabouter, woensdag 5 januari 2011 14:36

Aan de achterkant doet dit niets anders dan een GET naar de controller 'ClientActie' (GETs zijn goedkoper dan POSTs) via:

javascript:
$.post
Get is beter dus ik gebruik post?

[Reactie gewijzigd op woensdag 5 januari 2011 14:36]


Door Tweakers user creator1988, woensdag 5 januari 2011 14:40

skabouter schreef op woensdag 05 januari 2011 @ 14:36:
[...]
Get is beter dus ik gebruik post?
Dat 'uit-mijn-hoofd' code typen voor dit blog gaat me toch niet zo heel goed af :+

Door Tweakers user CodeCaster, woensdag 5 januari 2011 15:11

Oh, ik dacht dat dat met opzet was, vanwege je quote:
An interesting side affect is that POST without actually posting any data behaves like GET.

Door Tweakers user Niek.NET, woensdag 5 januari 2011 17:59

Worden nu niet alle objecten door zoekmachine spiders als favoriet gemarkeerd?

Door Tweakers user CodeCaster, woensdag 5 januari 2011 19:51

Niek.NET schreef op woensdag 05 januari 2011 @ 17:59:
Worden nu niet alle objecten door zoekmachine spiders als favoriet gemarkeerd?
Die zien enkel de url "?clientactie=bewaarobject" dus waarschijnlijk maar eenmalig.

Door Tweakers user YopY, woensdag 5 januari 2011 20:49

quote: CodeCaster
Die zien enkel de url "?clientactie=bewaarobject" dus waarschijnlijk maar eenmalig.
Is dat een 'ja'? :+

Ik neem aan dat favorieten opslaan alleen voor ingelogde gebruikers is (die cookies nodig hebben)? dan zal een zoekmachine daar niet zoveel moeite mee hebben. Tenzij die krengen tegenwoordig ook accounts maken om de rest van een site te indexeren.

POST vs GET is ook niet een kwestie van welke is goedkoper. POST is om dingen op te sturen, GET is om dingen op te halen - hence the name. Opslaan van een favoriet is, mijns insziens, het versturen van een keuze van de gebruiker naar de server, ergo, POST. Een andere manier om te achterhalen: Krijgt een gebruiker iets relevants als hij die URL direct aanroept?

Nee, POSTs kun je niet vanuit een link doen, dan moet je er een formulier(tje) van maken die naar ?clientactie=bewaarobject submit. Met CSS kun je dat net zo laten lijken als een gewone knop (als ik me niet vergis).

Als alternatief kun je nog een onclick op de a doen die een POST doet naar de serv... wacht, laat maar :+.

Nog één opmerking, overigens: in de onclick van die link (met fallback) zou ik wel altijd een 'return false' doen (of iets met event.preventDefault, weet zo niet wat dat doet eerlijk gezegd), anders dan krijg je dat beide acties uitgevoerd worden (AJAX request + gebruiker die op een linkje klikt).

Door Tweakers user moto-moi, donderdag 6 januari 2011 00:16

Waarom doe je niet een <select><option>optie1</option></select> en bouw je die via js om naar een van de 2 plaatjes van je? :) ( degene met de throbber lijkt me wat overdreven voor de fallback :+ )

Door Tweakers user creator1988, donderdag 6 januari 2011 10:47

Niek.NET schreef op woensdag 05 januari 2011 @ 17:59:
Worden nu niet alle objecten door zoekmachine spiders als favoriet gemarkeerd?
Nee, een zoekmachine heeft geen account. We verifieren in de 'ClientActieController' nog of iemand een account heeft etc. en anders wordt hij doorgestuurd naar de inloggenpagina.
YopY schreef op woensdag 05 januari 2011 @ 20:49:
POST vs GET is ook niet een kwestie van welke is goedkoper. POST is om dingen op te sturen, GET is om dingen op te halen - hence the name. Opslaan van een favoriet is, mijns insziens, het versturen van een keuze van de gebruiker naar de server, ergo, POST. Een andere manier om te achterhalen: Krijgt een gebruiker iets relevants als hij die URL direct aanroept?
Ja, de gebruiker krijgt wel degelijk iets relevants als hij ?clientactie=X invoert. Namelijk de pagina waarop hij net ook was, alleen nu met het item als favoriet gemarkeerd.
Nog één opmerking, overigens: in de onclick van die link (met fallback) zou ik wel altijd een 'return false' doen (of iets met event.preventDefault, weet zo niet wat dat doet eerlijk gezegd), anders dan krijg je dat beide acties uitgevoerd worden (AJAX request + gebruiker die op een linkje klikt).
Ja, lijkt me vanzelfsprekend :9 . In jQuery werken zowel 'return false' als

JavaScript:
1
$('a').click(function(ev) { ev.preventDefault(); });

moto-moi schreef op donderdag 06 januari 2011 @ 00:16:
Waarom doe je niet een <select><option>optie1</option></select> en bouw je die via js om naar een van de 2 plaatjes van je? :) ( degene met de throbber lijkt me wat overdreven voor de fallback :+ )
Uhh, wat is hier het voordeel van?

Door Tweakers user Niek.NET, donderdag 6 januari 2011 15:19

creator1988 schreef op donderdag 06 januari 2011 @ 10:47:
[...]

Nee, een zoekmachine heeft geen account. We verifieren in de 'ClientActieController' nog of iemand een account heeft etc. en anders wordt hij doorgestuurd naar de inloggenpagina.
Aha, ik snap het. Omdat je Profile gebruikte dacht ik dat het ging om een anoniem profiel ;)

Door Tweakers user crisp, donderdag 6 januari 2011 21:26

creator1988 schreef op donderdag 06 januari 2011 @ 10:47:
[...]
Uhh, wat is hier het voordeel van?
Wellicht dat moto-moi doelt op wat eigenlijk ook mijn eerste gedachte was bij de titel van deze blogpost: namelijk dat non-js als 'fallback' beschouwen eigenlijk een beetje het omgekeerde is van wat normaliter als 'best practice' wordt beschouwd met het oog op toegankelijkheid.

Ik ga bij development ook altijd uit van een situatie waarbij javascript geen rol speelt, en gebruik vervolgens javascript om gedrag toe te voegen en te vergemakkelijken (progressive enhancement) :)

Door Tweakers user crisp, donderdag 6 januari 2011 21:32

Oh, over GET versus POST; zie http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
In particular, the convention has been established that the GET and HEAD methods SHOULD NOT have the significance of taking an action other than retrieval.
Zie verder ook de uitleg over idempotence in die RFC. Daarnaast zijn acties die via GET kunnen worden uitgevoerd over het algemeen vatbaarder voor XSRF attacks.

Reageren is niet meer mogelijk