Here we turn our static web page into a dynamic one by printing data retrieved from an IoC service.
Static pages are pretty boring by any standard, and our current index page is static. It returns the same HTML every time it is rendered. So in this section we will make it dynamic and have it generate HTML based on data returned from an IoC service.
We will turn the index into a summary page and display a table of visit data.
- Create a Data Entity
- Create an IoC Service
- Print Data From Service
- Create Sample Data
- Run It
- Tidy Up
- Source Code
Create a Data Entity
The Bed Nap application will collect comments for an imaginary hotel, like an online visitors book. Therefore our collected data will contain:
- name
- date stayed
- a numerical rating (1 to 5)
- comment
We'll bundle this data into an entity class called Visit
:
class Visit { Str name Date date Int rating Str comment new make(Str name, Date date, Int rating, Str comment) { this.name = name this.date = date this.rating = rating this.comment = comment } }
An instance of Visit
should always have values for all the fields, so we make the field types not-nullable. This then means the field values have to be set in the ctor - it's a compile time error if we don't.
Create an IoC Service
We need a class to hold all our Visit
entities. A simplistic approach is to just hold them in a list:
class VisitService { private Visit[] visits := [,] Visit[] all() { visits } Void save(Visit visit) { visits.add(visit) } }
Every time our IndexPage
is rendered it will call VisitService.all()
, loop through the visits and print out the details. This means the same instance of VisitService
has to made available to every HTTP request; which means it needs to be thread-safe.
The Const is Cool section tells us that for our service class to be thread-safe it needs to be const
. But then how do we add data to the list?
Reading the From One Thread to Another... article informs us that the easiest way is to use a SynchronizedList from Alien-Factory's Concurrent library.
So let's download afConcurrent
:
fanr install -r http://eggbox.fantomfactory.org/fanr "afConcurrent 1.0.14 - 1.0"
And add it as a dependency to our build.fan
, locking it to version 1.0. You'll also need to add concurrent
too for ActorPool
.
Now we'll update our service class:
using afConcurrent const class VisitService { private const SynchronizedList visits := SynchronizedList(ActorPool()) { it.valType = Visit# } Visit[] all() { visits.list } Void save(Visit visit) { visits.add(visit) } }
Note that SynchronizedList
can only store immutable objects so we have turn our Visit
entity into a const
class:
const class Visit { const Str name const Date date const Int rating const Str comment new make(Str name, Date date, Int rating, Str comment) { this.name = name this.date = date this.rating = rating this.comment = comment } }
To make VisitService
a proper IoC service we need to define it in our AppModule
:
Void defineServices(RegistryBuilder bob) { bob.addService(VisitService#) }
See How To Build an IoC Service for more details:
Print Data From Service
Next we'll update our IndexPage
to print out the visit data.
But how do we access our VisitService
?
The IndexPage
is not just newed up, it is autobuilt by IoC. That means we can simply @Inject
the VisitService
:
using afIoc using afBedSheet const class IndexPage { @Inject private const VisitService visitService new make(|This|in) { in(this) } Text render() { html := """<!DOCTYPE html> <html> <head> <title>Bed Nap Index Page</title> </head> <body> <h1>Bed Nap Tutorial</h1> <table> <tr> <th>Name</th> <th>Date</th> <th>Rating</th> </tr> """ visitService.all.each { html += """<tr> <td>${it.name}</td> <td>${it.date}</td> <td>${it.rating}</td> </tr> """ } html += """</table> </body> </html> """ return Text.fromHtml(html) } }
Note that we use It-Block Injection to inject the service. It's the easiest way and is pretty standard.
Create Sample Data
We could run the web app now, but it wouldn't look much different because our VisitService
hasn't any visit data!
When BedSheet starts up, one of the first things it does is to build and startup the IoC registry. So if we hook into this event we can add some sample data to VisitService
. To do this we will contribute to RegistryStartup.
Void onRegistryStartup(Configuration config, VisitService visitService) { config["bednap.createSampleData"] = |->| { visitService.save(Visit("Traci Lords", Date(1986, Month.feb, 22), 5, "Loved the free back massage and exfoliating scrub!")) visitService.save(Visit("Ginger Lynn", Date(1996, Month.mar, 23), 3, "Room was large and clean but average.")) visitService.save(Visit("Vanessa del Rio", Date(2006, Month.apr, 24), 1, "Terrible. Occupants of the local prison have a better view.")) } }
Run It
We can now run the Bed Nap web app and view our dynamic page:
Okay... So it may not be that dynamic right now - but it could be! If the data held in the VisitService
were to change, then so would our index page.
Tidy Up
Now that everything is working nicely, we'll take the time to tidy up some code.
Use WebOutStream
All that string concatenation in IndexPage
is pretty ugly, so lets use a WebOutStream instead. Note you'll need to add a dependency on the web
pod in build.fan
.
using afIoc using afBedSheet using web::WebOutStream const class IndexPage { @Inject private const VisitService visitService new make(|This|in) { in(this) } Text render() { htmlBuf := StrBuf() html := WebOutStream(htmlBuf.out) html.docType5 html.html html.head html.title.w("Bed Nap Index Page").titleEnd html.headEnd html.body html.h1.w("Bed Nap Tutorial").h1End html.table html.tr html.th.w("Name").thEnd html.th.w("Date").thEnd html.th.w("Rating").thEnd html.trEnd visitService.all.each { html.tr html.td.w(it.name).tdEnd html.td.w(it.date).tdEnd html.td.w(it.rating).tdEnd html.trEnd } html.tableEnd html.bodyEnd html.htmlEnd return Text.fromHtml(htmlBuf.toStr) } }
The above code produces exactly the same HTML but looks much tidier.
Use ActorPools
In our VisitService
you'll notice we just new'ed up a instance of ActorPool
. As these classes potentially have a performance impact it's good to keep track / tabs on them. For that reason afConcurrent has an ActorPools service which does just that!
To use, we add an ActorPool
named bednap.visits
to the service in the AppModule
:
@Contribute { serviceType=ActorPools# } static Void contributeActorPools(Configuration config) { config["bednap.visits"] = ActorPool() { it.name = "bednap.visits"; it.maxThreads = 1 } }
We limit the max number of threads to 1 because, for our usage, we never need more than 1. It is used to create ONE Actor
for the SynchronizedList
.
Next we retrieve and use the same ActorPool
in VisitService
:
using afConcurrent using afIoc const class VisitService { private const SynchronizedList visits new make(ActorPools actorPools) { actorPool := actorPools["bednap.visits"] visits = SynchronizedList(actorPool) { it.listType = Visit# } } Visit[] all() { visits.list } Void save(Visit visit) { visits.add(visit) } }
Note that VisitService
now makes use of Ctor Injection.
Or a more succinct way is to make better use of afConcurrent's depenency providers:
using afConcurrent using afIoc const class VisitService { @Inject { id="bednap.visits"; type=Visit[]# } private const SynchronizedList visits new make(|This| f) { f(this) } ...
Note the service now uses an it-block ctor.
Source Code
All the source code for this tutorial is available on the Bed Nap Tutorial Bitbucket Repository.
Code for this particular article is available on the 03-Dynamic-Content branch.
Use the following commands to check the code out locally:
C:\> hg clone https://bitbucket.org/fantomfactory/bed-nap-tutorial C:\> cd bed-nap-tutorial C:\> hg update 03-Dynamic-Content
Don't forget, you can trial the finished tutorial application at Bed Nap.
Have fun!
Edits
- 3 Aug 2016 - Updated tutorial to use BedSheet 1.5 & IoC 3.0.
- 3 Aug 2015 - Updated tutorial to use BedSheet 1.4.
- 29 Aug 2014 - Original article.