2016-09-19 3 views
1

私のメインビューテンプレートでは、データベースからの動的なデータ、たとえばウェブサイトのナビゲーション項目を表示したいと考えています。すべてのコントローラでコードを重複させずにモデルをメインビューテンプレートに渡すにはどうすればよいですか?

モデルをテンプレートにパラメータとして追加すると、メインテンプレートを使用するすべてのビューでメインテンプレートのモデルを指定する必要があります。したがって、すべてのコントローラのすべてのアクションは、最初にメインテンプレートのナビゲーションモデルをフェッチする必要があります。

このアプローチは、すべてのアクションがメインテンプレートモデルを取得する方法を知る必要があるため、コードの重複と単一責任の原則の違反につながります。コードをテスト可能な状態に保ちながら、コードの重複なしに記述された機能を孤立した方法で提供する方法はありますか?

次モデル及びサービスクラスを模擬するために使用することができる。

package services 

import scala.concurrent.Future 

case class HeaderItem(title: String, url: String) 
case class User(name: String, email: String) 

class HeaderItemService { 
    val all: Future[Seq[HeaderItem]] = Future.successful(HeaderItem("Home", "/") :: Nil) 
} 

class UserService { 
    val all: Future[Seq[User]] = Future.successful(User("Test", "[email protected]") :: Nil) 
} 

メインビュー・テンプレートは、ヘッダ項目を表示:

@import services.HeaderItem 
@(headerItems: Seq[HeaderItem])(content: Html) 

<!DOCTYPE html> 
<html lang="en"> 
    <body> 
     <div id="header"> 
      <ul> 
      @for(item <- headerItems) { 
       <li>@item.title</li> 
      } 
      </ul> 
     </div> 
     @content 
    </body> 
</html> 

子ビューディスプレイ特定のデータ(ユーザー)を表示し、メインテンプレート固有のデータをテンプレートに渡す必要があります。

@import services.HeaderItem 
@import services.User 
@(headerItems: Seq[HeaderItem], users: Seq[User]) 

@main(headerItems) { 
    <ul> 
     @for(user <- users) { 
      <li>@user.name</li> 
     } 
    </ul> 
} 

そして、これはうまくナビゲーション項目だけでなく、ユーザーとして気にする必要がありますコントローラです。

package controllers 

import javax.inject._ 

import play.api.mvc._ 
import services.{HeaderItemService, UserService} 

import scala.concurrent.ExecutionContext.Implicits.global 

@Singleton 
class HomeController @Inject()(headerItemService: HeaderItemService, userService: UserService) extends Controller { 
    def index = Action.async { 
    for { 
     headerItems <- headerItemService.all 
     users <- userService.all 
    } yield Ok(views.html.index(headerItems, users)) 
    } 
} 

まずしようとする問題は、レンダリングにより近づくことができASP MVCで

Html.RenderActionメソッド(https://msdn.microsoft.com/en-us/library/ee839451(v=vs.100).aspx)を使用して、ビュー内のアクション。私が知る限り、同様のアプローチは、プレイフレームワーク(2.4)では不可能です。

+0

別のエンドポイントからヘッダ項目をフェッチするメインテンプレートでjavascriptを使用することができます –

+0

@Lukasz:javascriptを使用して、私は実際にASP RenderActionアプローチと同様のソリューションを構築できましたが、新しい依存関係をもたらし、読みにくい私はScalaだけに基づいたソリューションを好むだろう。 – Felix

答えて

1

重複を減らすためにコードを再編成する方法はいくつかあります。テンプレートは、指定されたパラメータから関数Htmlまでの単なる関数であることに留意してください。このことを念頭に置いて、あなたはこのようなあなたのコントローラーを整理することができます。

@Singleton 
class Renderer @Inject() (headerItemService: HeaderItemService) { 
    // wrap some content html with a layout with a menu 
    private def renderWithMenu (content: Html): Future[Html] = { 
    for { 
     headerItems <- headerItemService.all 
    } yield views.html.layoutWithMenu(headerItems, content) 
    } 
} 

@Singleton 
class HomeController @Inject()(userService: UserService, renderer: Renderer) extends Controller with ControllerOps { 
    def index = Action.async { 
    for { 
     users <- userService.all 
     // views.html.index now only contains the "content" html 
     rendered <- renderer.renderWithMenu(views.html.index(users)) 
    } yield Ok(rendered) 
    } 
} 

このコードはまだメニューのレンダリングを「トリガー」を担当していますが、アイテムを取得し、Htmlを製造するための責任がに移動されましたそれが再利用できる形質です。

Actionの構図については、テンプレートUIの場合にはちょっと残念だと思います。私は通常、より複雑なロジック(カスタムリクエストオブジェクト、パラメータの変更、認証など)を実行する認証やその他のコードのために予約します。

+0

Alvaro、あなたの回答をありがとう、これは私が探していたものです。ヘッダーをすべてのアクションからロードするコードを分けるためです。アクション構成を使用するアプローチと比較して、私はビューを通してヘッダーを渡す必要がないので、このソリューションが好きです。 最後に、私はバグです。私は、ControllerOps特性を使用するすべてのコントローラにHeaderItemServiceを注入する必要があります。依存関係をコントローラから外すことによってカップリングを減らす方法はありますか?私はその特性を抽象クラスにすることができると思うが、私は別の方法を好むだろう。 – Felix

+0

@Felixコードを更新しました。それが抽象クラスによって参照されているかどうかはわかりません。もしそうなら、それの問題は何ですか?終わりに、 'index'メソッドは"完全な "ページをレンダリングできる必要があります。他のコンポーネントに依存して助けてもらうことは意味があります。 –

0

機能の構成を使用してカスタムアクションを作成します。

getHeadersFromDBは、ユーザーがあまりにも長く待たなければならない場合にすぐに返されるdbコールです。それを最適化するか、いくつかのキャッシュ層を使用します。

def withHeadersAction(f: Headers => Request[AnyContent] => Future[Result]) = { 
    Action.async { req => 
     getHeadersFromDB.map { headers => 
      f(headers)(req) 
     }.recover { case th => Ok(s"oops error occurred ${th.getMessage}")} 
    } 
} 

どのようにこのカスタムアクションを使用する

ApplicationController @Inject()() extends Controller { 

    def foo = withHeadersAction { implicit headers => req => 
     Ok(views.html.something) //headers is implicitly passed to the view 
    } 

} 

implicitパラメータが明示的に渡すのparamsを取り除くために使用することができることを

something.scala.html

@(implicit headers: List[Headers]) 
@main("something") { 
    //doSomething() 
} 

その他の方法

私は唯一のアクションでコンテンツを作成し、あなた

がpamuのアプローチに基づいて、このviews.html.something(headers)(content)

def withHeadersAction(f: Request[AnyContent] => Future[Html]) = { 
     Action.async { req => 
      getHeadersFromDB.flatMap { headers => 
       f(req).map { content => Ok(views.html.something(headers)(content)} 
      }.recover { case th => Ok(s"oops error occurred ${th.getMessage}")} 
     } 
    } 

ApplicationController @Inject()() extends Controller { 

     def bar = withHeadersAction { req => 
      Future.successful(views.html.someContent()) 
     } 

    } 
+0

このアプローチは、ヘッダを別の関数にロードするコードをカプセル化します。しかし、私はメインビューを使用するビューにヘッダーアイテムを渡す必要があるので、Alvaroが提案したような特性を使用する方法を好むでしょう。 – Felix

+0

@Felix ..明示的にヘッダを渡すことを避けるために '暗黙的な' paramsを使うことができます。 – pamu

+0

@Felixが答えを編集しました。 – pamu

0

のようなビューを定義するためにすべてのものは、内部withHeadersActionで気を取られますカスタムアクションビルダーを使用して同様のアプローチを策定しました。

package controllers 

import javax.inject._ 

import play.api.mvc.{BodyParser, _} 
import play.twirl.api.Html 
import services.{HeaderItemService, UserService} 

import scala.concurrent.ExecutionContext.Implicits.global 
import scala.concurrent.Future 

class MainAction @Inject()(headerItemService: HeaderItemService) extends Results { 
    def apply(block: Request[AnyContent] => (Status, Future[Html])) = Action.async { request => 
    execute(request, block) 
    } 

    def apply[A](bodyParser: BodyParser[A])(block: Request[A] => (Status, Future[Html])) = Action.async(bodyParser) { request => 
    execute(request, block) 
    } 

    def execute[A](request: Request[A], block: Request[A] => (Status, Future[Html])) = { 
    val (status, futureContent) = block(request) 
    for { 
     content <- futureContent 
     headerItems <- headerItemService.all 
    } yield status(views.html.main(headerItems)(content)) 
    } 
} 

@Singleton 
class HomeController @Inject()(mainAction: MainAction, userService: UserService) extends Controller { 
    def index = mainAction { request => 
    val content = userService.all.map(users => views.html.index(users)) 
    (mainAction.Ok, content) 
    } 
} 

このアプローチには、メインビューテンプレートのレンダリングを担当する別のクラスへの依存関係の注入と、サブビューを表示するカスタムアクションからステータスを渡す可能性が含まれます。

関連する問題