An Introduction to Web Development with PLT Scheme - The Control
Resume
This is the fourth post in a series of posts on programming webapplications with PLT Scheme. In part one and two the model and view of a small Reddit-like application were discussed. Part three were about the basic operation of the PLT Scheme web-server. Here in part four we will finish the discussion of the controller. Finally I'll explain how to get this code from PLaneT, so you can run it on your own machine.
Don't forget to leave a comment, if you have questions or suggestions.
The Controller
The controller handles user interactions. The concrete actions in our application are:
- Viewing the front-page
- Up- and down-voting a link
- Viewing the page for submitting new links
- Submitting a new link
For each action, we'll have a function that invokes the appropriate function in the model, receives data back, and then let the view generate a web-page based on the data. The function handling the viewing of the front-page is simply:
(define (do-front-page)
(html-front-page 0 1 (page 0)))
The function page is from the model and html-front-page in the view (see earlier posts).
Implementing the controller
An user interaction is sent to the web-server in the form of a request. Given a request, the web-server determines which servlet is to handle the request, and then calls the servlet's start function.
Here we have a choice. Even though the controller conceptually is one entity, a common way to implement controllers is to have separate servlets for each action. In this tutorial however I'll implement the controller as a single servlet, and let it dispatch on the action. The url for the servlet will be:
http://localhost/servlets/control.scm
The action will be given as a parameter. One way to send the parameter is to include it as part of the link:
http://localhost/servlets/control.scm?action=submitnew
Another is to let action be a hidden field in a form.
The Start Function
After the receipt of a request the web-server starts the control-servlet by calling the start function with the request in
question. The web-framework provides us with a special form servlet, that allows us to to define the start function simply as:
(require (planet "web.scm" ("soegaard" "web.plt" 2 0))
(define start
(servlet
(dispatch-on-action)))
Here the servlet form evaluates to a function that
- receives a request
- stores the information in the request in various parameters
- evaluates its bodies, the result of the last body must be an x-expr
- construct a response based upon the x-expr and the contents of the various response parameters
During development it is mighty convenient to see any errors directly
in the browser. Therefore, let's introduce the form, with-errors-to-browser:
(define start
(servlet
(with-errors-to-browser
send/finish
dispatch-on-action))
Dispatching on actions
Any parameters sent as part of the request, are saved as bindings in current-bindings by the servlet-form in start. The dispatch code is therefore very simple:
(define (dispatch-on-action)
(with-binding (current-bindings) (action)
(match action
["updown" (do-updown)]
["submitnew" (do-submit-new)]
["submit" (do-submit)]
[else (do-front-page)])))
If the action is missing, the front page is shown.
Submital of new links
The action "submitnew" simply shows a page with a form, where a new url and title can be entered:
(define (do-submit-new)
(html-submit-new-page))
Hitting "submit" will POST the data to our servlet, and the action will be "submit". The controller must send the data to the model in order to add the new link. The user should return to the frontpage. One way to achieve this is as follows:
(define (do-submit)
(with-binding (current-bindings) (url title)
(when (and url title)
(insert-entry title url 10)))
(do-front-page))
Can you spot a problem?
The problem and its solution
What happens if the user hits the refresh button in their browser? The same data will be posted again. This is often referred to as the "Double Submit problem". The solution is to redirect the browser to the frontpage. If the user now hits refresh, then they'll just get the frontpage again.
(define (do-submit)
(with-binding (current-bindings) (url title)
(when (and url title)
(insert-entry title url 10)))
; to make sure a reload doesn't resubmit, we redirect to the front page
(current-redirect-temporarily "http://localhost/servlets/control.scm")
(html-redirect-page "Redirecting to frontpage"))
The redirection is put in place, by setting the parameter current-redirect-temporarily. When the servlet form constructs the reponse it checks whether any redirection parameters are set, if not it produces a normal "200 OK" response; however, if set, it produces a redirection response; e.g., in the case of current-redirect-temporarily, a "302 Found".
Note: You can also use current-redirect-see-other to get a "303 See Other" response.
Note: See
Redirect after Post by Michael Jouravlev for a thorough discussion of the POST-Redirect-GET technique.
Up- and Down-voting
The up- and down-arrows could be simple links, but if they were we would run into a similar problem with refresh. We therefore turn each link into a form, and handle its submital with the POST-Redirect-GET technique as before. The only complication is that big submital buttons aren't pretty; images of arrows are much prettier. This is solved with a little JavaScript (see the post on the view).
(define (do-updown)
; an arrow was clicked
(with-binding (current-bindings) (entry_id arrowitem)
(match arrowitem
["down" (when entry_id
(decrease-score entry_id))]
["up" (when entry_id
(increase-score entry_id))]
[else
'do-nothing]))
; to make sure a reload doesn't resubmit, we redirect to the front page
(current-redirect-temporarily (format "http://localhost:~a/servlets/control.scm" port))
(html-redirect-page "Redirecting to frontpage"))
Downloading the code
To download the code and try it, open DrScheme and enter the following in the interaction window:
(require (planet "install.scm" ("soegaard" "listit.plt" 1 0)))
Then call install to copy the web-site to a
folder of your choice:
> (install "c:/tmp/my-listit")
Copying the web-site to c:/tmp/my-listit .
Please wait...
The web-site has now been copied to c:/tmp/my-listit .
To test it, open "launch.scm" in DrScheme and run it.
Have fun!
Leave a comment, if you have questions or suggestions.