Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
586 views
in Technique[技术] by (71.8m points)

playframework 2.0 - How to avoid passing parameters everywhere in play2?

In play1, I usually get all data in actions, use them directly in views. Since we don't need to explicitly declare parameters in view, this is very easy.

But in play2, I found we have to declare all the parameters(including request) in the head of views, it will be very boring to get all data in actions and pass them into views.

For example, if I need to display menus which are loaded from database in the front page, I have to define it in main.scala.html:

@(title: String, menus: Seq[Menu])(content: Html)    

<html><head><title>@title</title></head>
<body>
    <div>
    @for(menu<-menus) {
       <a href="#">@menu.name</a>
    }
    </div>
    @content
</body></html>

Then I have to declare it in every sub page:

@(menus: Seq[Menu])

@main("SubPage", menus) {
   ...
}

Then I have to get the menus and pass it to view in every action:

def index = Action {
   val menus = Menu.findAll()
   Ok(views.html.index(menus))
}

def index2 = Action {
   val menus = Menu.findAll()
   Ok(views.html.index2(menus))
}

def index3 = Action {
   val menus = Menu.findAll()
   Ok(views.html.index(menus3))
}

For now it's only one parameter in main.scala.html, what if there are many?

So at last, I decided to all Menu.findAll() directly in view:

@(title: String)(content: Html)    

<html><head><title>@title</title></head>
<body>
    <div>
    @for(menu<-Menu.findAll()) {
       <a href="#">@menu.name</a>
    }
    </div>
    @content
</body></html>

I don't know if it is good or recommended, is there any better solution for this?

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

In my opinion, the fact that templates are statically typed is actually a good thing: you’re guaranteed that calling your template will not fail if it compiles.

However, it indeed adds some boilerplate on the calling sites. But you can reduce it (without losing static typing advantages).

In Scala, I see two ways to achieve it: through actions composition or by using implicit parameters. In Java I suggest using the Http.Context.args map to store useful values and retrieve them from the templates without having to explicitly pass as templates parameters.

Using implicit parameters

Place the menus parameter at the end of your main.scala.html template parameters and mark it as “implicit”:

@(title: String)(content: Html)(implicit menus: Seq[Menu])    

<html>
  <head><title>@title</title></head>
  <body>
    <div>
      @for(menu<-menus) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>

Now if you have templates calling this main template, you can have the menus parameter implicitly passed for you to the main template by the Scala compiler if it is declared as an implicit parameter in these templates as well:

@()(implicit menus: Seq[Menu])

@main("SubPage") {
  ...
}

But if you want to have it implicitly passed from your controller you need to provide it as an implicit value, available in the scope from where you call the template. For instance, you can declare the following method in your controller:

implicit val menu: Seq[Menu] = Menu.findAll

Then in your actions you’ll be able to just write the following:

def index = Action {
  Ok(views.html.index())
}

def index2 = Action {
  Ok(views.html.index2())
}

You can find more information about this approach in this blog post and in this code sample.

Update: A nice blog post demonstrating this pattern has also been written here.

Using actions composition

Actually, it’s often useful to pass the RequestHeader value to the templates (see e.g. this sample). This does not add so much boilerplate to your controller code because you can easily write actions receiving an implicit request value:

def index = Action { implicit request =>
  Ok(views.html.index()) // The `request` value is implicitly passed by the compiler
}

So, since templates often receive at least this implicit parameter, you could replace it with a richer value containing e.g. your menus. You can do that by using the actions composition mechanism of Play 2.

To do that you have to define your Context class, wrapping an underlying request:

case class Context(menus: Seq[Menu], request: Request[AnyContent])
        extends WrappedRequest(request)

Then you can define the following ActionWithMenu method:

def ActionWithMenu(f: Context => Result) = {
  Action { request =>
    f(Context(Menu.findAll, request))
  }
}

Which can be used like this:

def index = ActionWithMenu { implicit context =>
  Ok(views.html.index())
}

And you can take the context as an implicit parameter in your templates. E.g. for main.scala.html:

@(title: String)(content: Html)(implicit context: Context)

<html><head><title>@title</title></head>
  <body>
    <div>
      @for(menu <- context.menus) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>

Using actions composition allows you to aggregate all the implicit values your templates require into a single value, but on the other hand you can lose some flexibility…

Using Http.Context (Java)

Since Java does not have Scala’s implicits mechanism or similar, if you want to avoid to explicitly pass templates parameters a possible way is to store them in the Http.Context object which lives only for the duration of a request. This object contains an args value of type Map<String, Object>.

Thus, you can start by writing an interceptor, as explained in the documentation:

public class Menus extends Action.Simple {

    public Result call(Http.Context ctx) throws Throwable {
        ctx.args.put("menus", Menu.find.all());
        return delegate.call(ctx);
    }

    public static List<Menu> current() {
        return (List<Menu>)Http.Context.current().args.get("menus");
    }
}

The static method is just a shorthand to retrieve the menus from the current context. Then annotate your controller to be mixed with the Menus action interceptor:

@With(Menus.class)
public class Application extends Controller {
    // …
}

Finally, retrieve the menus value from your templates as follows:

@(title: String)(content: Html)
<html>
  <head><title>@title</title></head>
  <body>
    <div>
      @for(menu <- Menus.current()) {
        <a href="#">@menu.name</a>
      }
    </div>
    @content
  </body>
</html>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
...