如何在每个控制器中没有代码重复的情况下将模型传递到主视图模板?

时间:2016-09-19 15:01:37

标签: scala playframework-2.4

在我的主视图模板中,我想显示动态数据(来自数据库),例如网站的导航项。

当我将模型作为参数添加到模板时,需要使用我的主模板的每个视图来提供主模板的模型。因此,每个控制器中的每个操作首先需要获取主模板的导航模型。

这种方法会导致代码重复和违反单一责任原则,因为每个操作都需要知道如何检索主模板模型。有没有办法以隔离的方式提供所描述的功能而无需代码重复,同时保持代码可测试?

示例

以下内容可用于模拟模型和服务类:

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", "test@test") :: 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)在视图内渲染动作来解决问题。据我所知,play框架(2.4)无法实现类似的方法。

3 个答案:

答案 0 :(得分: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的东西有点过分。我通常保留用于身份验证或执行更复杂逻辑的其他代码(自定义请求对象,修改参数,授权等)。

答案 1 :(得分: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参数可用于明确删除传递的参数

something.scala.html

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

其他方式

只在动作中创建内容,并且内部为您提供的所有内容都将由您处理

定义类似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())
       }

    }

答案 2 :(得分:0)

基于pamu的方法,我使用自定义动作构建器制定了类似的方法:

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)
  }
}

此方法包括将依赖项注入到负责呈现主视图模板的单独类中,以及从呈现子视图的自定义操作传递状态的可能性。