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

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:

select all
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:

select all
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:

select all
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:

select all
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:

select all
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:

Bed Nap Screenshot

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.

select all
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:

select all
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:

select all
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.


Discuss