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

Альваро, спасибо за ваш ответ, это в значительной степени то, что я искал, поскольку он отделяет код загрузки заголовков от всех действий. По сравнению с подходом к использованию композиции действий, я предпочитаю это решение, потому что мне не нужно передавать заголовки через представления. Тем не менее, последнее, что меня беспокоит: мне все равно нужно вводить HeaderItemService в любой контроллер, который будет использовать признак ControllerOps. Есть ли способ уменьшить сцепление, перемещая зависимость от контроллеров? Наверное, я мог бы сделать черту абстрактным классом, но я предпочел бы другой путь. – 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() 
} 

Другой способ

только создавать контент в действии, и все будет позаботилась по withHeadersAction внутренне для вас

определяют вид как этот 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

Этот подход будет инкапсулировать код для загрузки заголовков в отдельную функцию, что приятно. Но тем не менее, мне нужно будет передать элементы заголовка в любое представление, которое использует основной вид, поэтому я предпочел бы использовать черту, как предложил Альваро. – Felix

+0

@Felix .. вы можете использовать 'implicit' params, чтобы избавиться от пропущенных заголовков явно. – pamu

+0

@Felix отредактировал ответ. Пожалуйста, проверьте – pamu

0

на основе подхода I 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) 
    } 
} 

Этот подход включает в себя инъекции зависимостей в отдельный класс, который отвечает за отображение основного шаблона представления, а также возможность передать статус из пользовательских действий, которые отображают под-представления.

 Смежные вопросы

  • Нет связанных вопросов^_^