securesocial icon indicating copy to clipboard operation
securesocial copied to clipboard

[3.0-M1] Examples needed for views overriding

Open tavlima opened this issue 10 years ago • 9 comments

I'm trying to supply new templates for the multiple views, without success. The biggest issue seems to be related to not available contexts and requests. I tried to write new ViewTemplates both in Java and Scala, but it didn't work. I could get a little bit further with Scala, but had the same issues as Java.

@jaliss, I think we all would appreciate some examples on that. Would it be possible to add some view customization to the demo app? Having both Java and Scala implementations of the ViewTemplates trait would be a big plus.

Sorry for asking that, instead of contributing, but be sure that I would, if I had any success.

tavlima avatar Sep 06 '14 00:09 tavlima

@tavlima the views are supposed to be written in Scala. I have custom views working in a project. I have not tried to implement them in Java. The controllers using the views are based in Scala so I don't think writing them directly in Java will make them work.

Can you share a stack trace or more information of what's going on? A sample project showing this fail would be great.

I will try to view customization in the sample app so others can use as a guide. I will be with limited internet access the following days so I won't be able to tackle this right away.

jaliss avatar Sep 06 '14 02:09 jaliss

@jaliss, sorry for the delay but, after a while, I managed to extend the ViewTemplates using Scala and could point to my own templates. It was a bit painful, as I'm not Scala fluent. The examples I asked for would be really helpful.

tavlima avatar Oct 23 '14 15:10 tavlima

Were any examples posted ?

JavonDavis avatar Apr 06 '15 08:04 JavonDavis

@jaliss examples for extending ViewTemplates to customise views (scala is fine) against 3.0-M4 would be really useful, as this seems to be difficult for a number of people. I've been working at this for a day or so, reviewing the posts in the SecureSocial google group and so far am unable to get compilation between the def overrides in my customised environment with custom view.scala.html code. Typical fails with either "ambigous implicit variable" compile time error, or "could not find implicit value for parameter env: securesocial.core.RuntimeEnvironment" compile time error.

sdk451 avatar May 23 '16 05:05 sdk451

Hi sdk451,

Here are the basic steps to customize your templates:

  1. Create the views that you want to customize. You can copy the views from this folder : https://github.com/jaliss/securesocial/tree/3.0-M3/module-code/app/securesocial/views and modify them
  2. Create a class MyViewTemplates where you reference the pages you just created instead of referencing securesocial.views This file is based on : https://github.com/jaliss/securesocial/blob/3.0-M3/module-code/app/securesocial/controllers/ViewsPlugin.scala

Example:

class MyViewTemplates(env:RuntimeEnvironment[_]) extends ViewTemplates{


  implicit val implicitEnv = env


  override def getLoginPage(form: Form[(String, String)], msg:

Option[String])(implicit request: RequestHeader, lang: Lang): Html = {

   * views.html.login(form, msg)(request, lang, env) // This is a new

page for example*

  }



  override def getSignUpPage(form: Form[RegistrationInfo], token:

String)(implicit request: RequestHeader, lang: Lang): Html = {

    //securesocial.views.html.Registration.signUp(form, token)(request,

lang, env)

     views.html.Registration.signUp(form, token)(request, lang, env)

  }


  override def getStartSignUpPage(form: Form[String])(implicit request:

RequestHeader, lang: Lang): Html = {

    Logger.debug("Calling startSignUp page")

  views.html.startSignUp(form)(request, lang, env)

    //securesocial.views.html.Registration.startSignUp(form)(request,

lang, env)

  }


  override def getStartResetPasswordPage(form: Form[String])(implicit

request: RequestHeader, lang: Lang): Html = {

    Logger.debug("Calling Custom getStartRestePasswordPage...")

    views.html.Registration.startResetPassword(form)(request, lang, env)

//securesocial.views.html.Registration.startResetPassword(form)(request, lang, env)

  }


  override def getResetPasswordPage(form: Form[(String, String)],

token: String)(implicit request: RequestHeader, lang: Lang): Html = {

    //securesocial.views.html.Registration.resetPasswordPage(form,

token)(request, lang, env)

    views.html.Registration.resetPasswordPage(form, token)(request,

lang, env)

  }


  override def getPasswordChangePage(form: Form[ChangeInfo])(implicit

request: RequestHeader, lang: Lang): Html = {

    views.html.Registration.passwordChange(form)(request, lang, env)

  }


  override def getNotAuthorizedPage(implicit request: RequestHeader,

lang: Lang): Html = {

    securesocial.views.html.notAuthorized()(request, lang, env)

  }



}
  1. In your Global.scala, add a reference to the new Template

object MyRuntimeEnvironment extends RuntimeEnvironment.Default[SwUser] {

override implicit val executionContext =

play.api.libs.concurrent.Execution.defaultContext

override lazy val routes = new CustomRoutesService()

override lazy val userService: SwUserService = new SwUserService()

override lazy val eventListeners = List(new UserEventListener())

*override lazy val viewTemplates: ViewTemplates = new

MyViewTemplates(this)*

...

}

Repeat the steps for MailTemplate :)

Hope this helps

Olivier

Olivier Droz Ing. Inf. Dipl. EPF Rte de la Feuillère 29 1010 Lausanne www.olivierdroz.ch

On Mon, May 23, 2016 at 7:40 AM, sdk451 [email protected] wrote:

@jaliss https://github.com/jaliss examples for extending ViewTemplates to customise views (scala is fine) against 3.0-M4 would be really useful, as this seems to be difficult for a number of people. I've been working at this for a day or so, reviewing the posts in the SecureSocial google group and so far am unable to get compilation between the def overrides in my customised environment with custom view.scala.html code. Typical fails with either "ambigous implicit variable" compile time error, or "could not find implicit value for parameter env: securesocial.core.RuntimeEnvironment" compile time error.

— You are receiving this because you are subscribed to this thread. Reply to this email directly or view it on GitHub https://github.com/jaliss/securesocial/issues/464#issuecomment-220891419

odroz avatar May 23 '16 06:05 odroz

Thanks for answering Olivier.

I was using securesocial-3.0-M4 which doesn't have a templated RuntimeEnvironment. I switched back to 3.0-M3, and closely followed your code snippets. Unfortunately I still get ambigous implicit values for the RequestHeader var.

My code (I am running java, except for this section which is in scala):

MyViewTemplates.scala:

package service;

import securesocial.controllers.{ChangeInfo, RegistrationInfo, MailTemplates, ViewTemplates} import securesocial.core.{ BasicProfile, RuntimeEnvironment } import play.api.mvc.{RequestHeader, Request} import play.twirl.api.{ Html, Txt } import play.api.data.Form import play.api.i18n.Lang //import play.api.Play.current

import views.html.custom.login

class MyViewTemplates(env: RuntimeEnvironment[_]) extends ViewTemplates.Default(env) { implicit val implicitEnv = env

override def getLoginPage(form: play.api.data.Form[(String, String)],

msg: Option[String])(implicit request: RequestHeader, lang: Lang): Html = { views.html.custom.login(form, msg)(request, lang, env) } }

MyEnvironment.scala:

package service

//import javax.inject._ import com.google.inject.Inject import securesocial.core.RuntimeEnvironment import securesocial.core.services.UserService

import securesocial.controllers.{MailTemplates, ViewTemplates} import securesocial.core.authenticator._ import securesocial.core.providers._ import securesocial.core.providers.utils.{Mailer, PasswordHasher, PasswordValidator} import securesocial.core.services._

import scala.collection.immutable.ListMap

class MyEnvironment @Inject() extends RuntimeEnvironment.Default[UserCredential] { override implicit val executionContext = play.api.libs.concurrent.Execution.defaultContext

type U = UserCredential

override val userService: UserService[U] = new MyUserService()

// override lazy val routes = new CustomRoutesService()

override lazy val viewTemplates = new MyViewTemplates(this)

override lazy val providers = ListMap(include(new

UsernamePasswordProvider[U](userService, avatarService, viewTemplates, passwordHashers)))

}

// UserCredential is my implementation of DemoUser

Global.java:

import securesocial.core.RuntimeEnvironment; import service.MyEnvironment;

public class Global extends GlobalSettings {

@Singleton
public static RuntimeEnvironment env = new MyEnvironment();

/// bunch of other stuff not relevant to secure social setup

}

and here is the custom login.scala.html which is a direct copy of the login view from securesocial 3.0-M3. I have created a custom folder in my views.html package - it has minor tweaks to the @ inputs - request needs to be play.api.mvc.RequestHeader - probably because the Play java and scala apis are packaged differently (scala - play.api., java - play.)

\custom\login.scala.html:

@(loginForm: play.api.data.Form[(String,String)], errorMsg: Option[String] = None)(implicit request: play.api.mvc.RequestHeader, lang: play.api.i18n.Lang, env:securesocial.core.RuntimeEnvironment[_])

@import securesocial.core.providers.UsernamePasswordProvider.UsernamePassword

@securesocial.views.html.main(Messages("securesocial.login.title")) {

@errorMsg.map { msg =>
    <div class="alert alert-error">
        @Messages(msg)
    </div>
}

@request.flash.get("success").map { msg =>
    <div class="alert alert-info">
        @msg
    </div>
}

@request.flash.get("error").map { msg =>
    <div class="alert alert-error">
        @msg
    </div>
}

@defining( env.providers.values.filter( _.id != UsernamePassword) ) { externalProviders =>

    @if( externalProviders.size > 0 ) {
        <div class="clearfix">
            <p>@Messages("securesocial.login.instructions")</p>
            <p>
                @for(p <- externalProviders) {
                    @securesocial.views.html.provider(p.id)
                }
            </p>
        </div>
    }

    @env.providers.get(UsernamePassword).map { up =>
        <div class="clearfix">
            @if( externalProviders.size > 0 ) {

@Messages("securesocial.login.useEmailAndPassword")

            } else {

@Messages("securesocial.login.useEmailAndPasswordOnly")

            }

           @securesocial.views.html.provider("userpass",

Some(loginForm)) } } }

Not sure if the imports are causing something odd in the implicit scope (could conceiveably be different between scala and java)

finally the compiler error is:

  • read from stdout: ...\app\views\custom\login.scala.html:38: ambiguous implicit values:
  • Read from stdout: both method requestHeader in object PlayMagicForJava of type => play.api.mvc.RequestHeader
  • Read from stdout: and value request of type play.api.mvc.RequestHeader
  • Read from stdout: match expected type play.api.mvc.RequestHeader
  • ....app\views\custom\login.scala.html:38: ambiguous implicit values: both method requestHeader in object PlayMagicForJava of type => play.api.mvc.RequestHeader and value request of type play.api.mvc.RequestHeader match expected type play.api.mvc.RequestHeader
  • Read from stdout:
  • Read from stdout: ^

On Mon, May 23, 2016 at 4:54 PM, Olivier Dorz [email protected] wrote:

Hi sdk451,

Here are the basic steps to customize your templates:

  1. Create the views that you want to customize. You can copy the views from this folder :

https://github.com/jaliss/securesocial/tree/3.0-M3/module-code/app/securesocial/views and modify them

  1. Create a class MyViewTemplates where you reference the pages you just created instead of referencing securesocial.views This file is based on :

https://github.com/jaliss/securesocial/blob/3.0-M3/module-code/app/securesocial/controllers/ViewsPlugin.scala

Example:

class MyViewTemplates(env:RuntimeEnvironment[_]) extends ViewTemplates{

implicit val implicitEnv = env

override def getLoginPage(form: Form[(String, String)], msg: Option[String])(implicit request: RequestHeader, lang: Lang): Html = {

  • views.html.login(form, msg)(request, lang, env) // This is a new page for example*

}

override def getSignUpPage(form: Form[RegistrationInfo], token: String)(implicit request: RequestHeader, lang: Lang): Html = {

//securesocial.views.html.Registration.signUp(form, token)(request, lang, env)

views.html.Registration.signUp(form, token)(request, lang, env)

}

override def getStartSignUpPage(form: Form[String])(implicit request: RequestHeader, lang: Lang): Html = {

Logger.debug("Calling startSignUp page")

views.html.startSignUp(form)(request, lang, env)

//securesocial.views.html.Registration.startSignUp(form)(request, lang, env)

}

override def getStartResetPasswordPage(form: Form[String])(implicit request: RequestHeader, lang: Lang): Html = {

Logger.debug("Calling Custom getStartRestePasswordPage...")

views.html.Registration.startResetPassword(form)(request, lang, env)

//securesocial.views.html.Registration.startResetPassword(form)(request, lang, env)

}

override def getResetPasswordPage(form: Form[(String, String)], token: String)(implicit request: RequestHeader, lang: Lang): Html = {

//securesocial.views.html.Registration.resetPasswordPage(form, token)(request, lang, env)

views.html.Registration.resetPasswordPage(form, token)(request, lang, env)

}

override def getPasswordChangePage(form: Form[ChangeInfo])(implicit request: RequestHeader, lang: Lang): Html = {

views.html.Registration.passwordChange(form)(request, lang, env)

}

override def getNotAuthorizedPage(implicit request: RequestHeader, lang: Lang): Html = {

securesocial.views.html.notAuthorized()(request, lang, env)

}

}

  1. In your Global.scala, add a reference to the new Template

object MyRuntimeEnvironment extends RuntimeEnvironment.Default[SwUser] {

override implicit val executionContext = play.api.libs.concurrent.Execution.defaultContext

override lazy val routes = new CustomRoutesService()

override lazy val userService: SwUserService = new SwUserService()

override lazy val eventListeners = List(new UserEventListener())

override lazy val viewTemplates: ViewTemplates = new MyViewTemplates(this)

...

}

Repeat the steps for MailTemplate :)

Hope this helps

Olivier

Olivier Droz Ing. Inf. Dipl. EPF Rte de la Feuillère 29 1010 Lausanne www.olivierdroz.ch

On Mon, May 23, 2016 at 7:40 AM, sdk451 [email protected] wrote:

@jaliss https://github.com/jaliss examples for extending ViewTemplates to customise views (scala is fine) against 3.0-M4 would be really useful, as this seems to be difficult for a number of people. I've been working at this for a day or so, reviewing the posts in the SecureSocial google group and so far am unable to get compilation between the def overrides in my customised environment with custom view.scala.html code. Typical fails with either "ambigous implicit variable" compile time error, or "could not find implicit value for parameter env: securesocial.core.RuntimeEnvironment" compile time error.

— You are receiving this because you are subscribed to this thread. Reply to this email directly or view it on GitHub < https://github.com/jaliss/securesocial/issues/464#issuecomment-220891419>

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/jaliss/securesocial/issues/464#issuecomment-220900708

sdk451 avatar May 24 '16 03:05 sdk451

Looks like more an ambiguous reference to a requestHeader that is also in PlayMagicForJava. I never used PlayMagicForJava. Do you really need that?

Olivier Droz Ing. Inf. Dipl. EPF Rte de la Feuillère 29 1010 Lausanne www.olivierdroz.ch

On Tue, May 24, 2016 at 5:16 AM, sdk451 [email protected] wrote:

Thanks for answering Olivier.

I was using securesocial-3.0-M4 which doesn't have a templated RuntimeEnvironment. I switched back to 3.0-M3, and closely followed your code snippets. Unfortunately I still get ambigous implicit values for the RequestHeader var.

My code (I am running java, except for this section which is in scala):

MyViewTemplates.scala:

package service;

import securesocial.controllers.{ChangeInfo, RegistrationInfo, MailTemplates, ViewTemplates} import securesocial.core.{ BasicProfile, RuntimeEnvironment } import play.api.mvc.{RequestHeader, Request} import play.twirl.api.{ Html, Txt } import play.api.data.Form import play.api.i18n.Lang //import play.api.Play.current

import views.html.custom.login

class MyViewTemplates(env: RuntimeEnvironment[_]) extends ViewTemplates.Default(env) { implicit val implicitEnv = env

override def getLoginPage(form: play.api.data.Form[(String, String)], msg: Option[String])(implicit request: RequestHeader, lang: Lang): Html = { views.html.custom.login(form, msg)(request, lang, env) } }

MyEnvironment.scala:

package service

//import javax.inject._ import com.google.inject.Inject import securesocial.core.RuntimeEnvironment import securesocial.core.services.UserService

import securesocial.controllers.{MailTemplates, ViewTemplates} import securesocial.core.authenticator._ import securesocial.core.providers._ import securesocial.core.providers.utils.{Mailer, PasswordHasher, PasswordValidator} import securesocial.core.services._

import scala.collection.immutable.ListMap

class MyEnvironment @Inject() extends RuntimeEnvironment.Default[UserCredential] { override implicit val executionContext = play.api.libs.concurrent.Execution.defaultContext

type U = UserCredential

override val userService: UserService[U] = new MyUserService()

// override lazy val routes = new CustomRoutesService()

override lazy val viewTemplates = new MyViewTemplates(this)

override lazy val providers = ListMap(include(new UsernamePasswordProvider[U](userService, avatarService, viewTemplates, passwordHashers)))

}

// UserCredential is my implementation of DemoUser

Global.java:

import securesocial.core.RuntimeEnvironment; import service.MyEnvironment;

public class Global extends GlobalSettings {

@Singleton public static RuntimeEnvironment env = new MyEnvironment();

/// bunch of other stuff not relevant to secure social setup

}

and here is the custom login.scala.html which is a direct copy of the login view from securesocial 3.0-M3. I have created a custom folder in my views.html package - it has minor tweaks to the @ inputs - request needs to be play.api.mvc.RequestHeader - probably because the Play java and scala apis are packaged differently (scala - play.api., java - play.)

\custom\login.scala.html:

@(loginForm: play.api.data.Form[(String,String)], errorMsg: Option[String] = None)(implicit request: play.api.mvc.RequestHeader, lang: play.api.i18n.Lang, env:securesocial.core.RuntimeEnvironment[_])

@import securesocial.core.providers.UsernamePasswordProvider.UsernamePassword

@securesocial.views.html.main(Messages("securesocial.login.title")) {

@errorMsg.map { msg =>

@Messages(msg)

}

@request.flash.get("success").map { msg =>

@msg

}

@request.flash.get("error").map { msg =>

@msg

}

@defining( env.providers.values.filter( _.id != UsernamePassword) ) { externalProviders =>

@if( externalProviders.size > 0 ) {

@Messages("securesocial.login.instructions")

@for(p

}

@env.providers.get(UsernamePassword).map { up =>

@if( externalProviders.size > 0 ) {

@Messages("securesocial.login.useEmailAndPassword")

} else {

@Messages("securesocial.login.useEmailAndPasswordOnly")

} @securesocial.views.html.provider("userpass", Some(loginForm))

} } }

Not sure if the imports are causing something odd in the implicit scope (could conceiveably be different between scala and java)

finally the compiler error is:

  • read from stdout: ...\app\views\custom\login.scala.html:38: ambiguous implicit values:
  • Read from stdout: both method requestHeader in object PlayMagicForJava of type => play.api.mvc.RequestHeader
  • Read from stdout: and value request of type play.api.mvc.RequestHeader
  • Read from stdout: match expected type play.api.mvc.RequestHeader
  • ....app\views\custom\login.scala.html:38: ambiguous implicit values: both method requestHeader in object PlayMagicForJava of type => play.api.mvc.RequestHeader and value request of type play.api.mvc.RequestHeader match expected type play.api.mvc.RequestHeader
  • Read from stdout:
  • Read from stdout: ^

On Mon, May 23, 2016 at 4:54 PM, Olivier Dorz [email protected] wrote:

Hi sdk451,

Here are the basic steps to customize your templates:

  1. Create the views that you want to customize. You can copy the views from this folder :

https://github.com/jaliss/securesocial/tree/3.0-M3/module-code/app/securesocial/views and modify them

  1. Create a class MyViewTemplates where you reference the pages you just created instead of referencing securesocial.views This file is based on :

https://github.com/jaliss/securesocial/blob/3.0-M3/module-code/app/securesocial/controllers/ViewsPlugin.scala

Example:

class MyViewTemplates(env:RuntimeEnvironment[_]) extends ViewTemplates{

implicit val implicitEnv = env

override def getLoginPage(form: Form[(String, String)], msg: Option[String])(implicit request: RequestHeader, lang: Lang): Html = {

  • views.html.login(form, msg)(request, lang, env) // This is a new page for example*

}

override def getSignUpPage(form: Form[RegistrationInfo], token: String)(implicit request: RequestHeader, lang: Lang): Html = {

//securesocial.views.html.Registration.signUp(form, token)(request, lang, env)

views.html.Registration.signUp(form, token)(request, lang, env)

}

override def getStartSignUpPage(form: Form[String])(implicit request: RequestHeader, lang: Lang): Html = {

Logger.debug("Calling startSignUp page")

views.html.startSignUp(form)(request, lang, env)

//securesocial.views.html.Registration.startSignUp(form)(request, lang, env)

}

override def getStartResetPasswordPage(form: Form[String])(implicit request: RequestHeader, lang: Lang): Html = {

Logger.debug("Calling Custom getStartRestePasswordPage...")

views.html.Registration.startResetPassword(form)(request, lang, env)

//securesocial.views.html.Registration.startResetPassword(form)(request, lang, env)

}

override def getResetPasswordPage(form: Form[(String, String)], token: String)(implicit request: RequestHeader, lang: Lang): Html = {

//securesocial.views.html.Registration.resetPasswordPage(form, token)(request, lang, env)

views.html.Registration.resetPasswordPage(form, token)(request, lang, env)

}

override def getPasswordChangePage(form: Form[ChangeInfo])(implicit request: RequestHeader, lang: Lang): Html = {

views.html.Registration.passwordChange(form)(request, lang, env)

}

override def getNotAuthorizedPage(implicit request: RequestHeader, lang: Lang): Html = {

securesocial.views.html.notAuthorized()(request, lang, env)

}

}

  1. In your Global.scala, add a reference to the new Template

object MyRuntimeEnvironment extends RuntimeEnvironment.Default[SwUser] {

override implicit val executionContext = play.api.libs.concurrent.Execution.defaultContext

override lazy val routes = new CustomRoutesService()

override lazy val userService: SwUserService = new SwUserService()

override lazy val eventListeners = List(new UserEventListener())

override lazy val viewTemplates: ViewTemplates = new MyViewTemplates(this)

...

}

Repeat the steps for MailTemplate :)

Hope this helps

Olivier

Olivier Droz Ing. Inf. Dipl. EPF Rte de la Feuillère 29 1010 Lausanne www.olivierdroz.ch

On Mon, May 23, 2016 at 7:40 AM, sdk451 [email protected] wrote:

@jaliss https://github.com/jaliss examples for extending ViewTemplates to customise views (scala is fine) against 3.0-M4 would be really useful, as this seems to be difficult for a number of people. I've been working at this for a day or so, reviewing the posts in the SecureSocial google group and so far am unable to get compilation between the def overrides in my customised environment with custom view.scala.html code. Typical fails with either "ambigous implicit variable" compile time error, or "could not find implicit value for parameter env: securesocial.core.RuntimeEnvironment" compile time error.

— You are receiving this because you are subscribed to this thread. Reply to this email directly or view it on GitHub < https://github.com/jaliss/securesocial/issues/464#issuecomment-220891419

— You are receiving this because you commented. Reply to this email directly or view it on GitHub < https://github.com/jaliss/securesocial/issues/464#issuecomment-220900708>

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/jaliss/securesocial/issues/464#issuecomment-221156065

odroz avatar May 24 '16 08:05 odroz

PlayMagicForJava is brought in by default by the Play framework if you are using Java to help with the Twirl templating (in scala). One of the subtle play framework differences for java users.

I finally figured out that the compile issues weren't with my ViewTemplates subclass or the overriden methods in it (had to ensure the method signatures were aligned correctly with ViewTemplates.Default), but actually the implicit scope vars being passed through my custom login.scala.html template into the main.scala.html and provider,scala.html sub templates within my login.scala.html. (Still pretty weak on the scala side of Play!) Basically the request and lang implicit vars are also provided by the playframework to any view template, so the compiler couldn't decide which of the ones passed in from MyViewTemplates or the framework to pass to main or provider.

I will post up my final play 2.4.2 for java solution (on the secure social google group) when I've finished and tested it, so others can see a complete working example.

Thanks for looking this, I appreciate the help.

sdk451 avatar May 24 '16 21:05 sdk451

May have spoken too early. So far - I can get custom views working for java with v3.0-M3 (including re-using securesocial view templates like provider and main) but this version has a dependency injection runtime error that I haven't been able to fix. v3.0-M4 java templating doesn't appear to work if you want to reuse existing securesocial templates like provider or main, as these require an implicit play.api.i18n.Messages object as an input which conflicts with the Java play.i18n.Messages implictMessages variable passed into templates by PlayMagicForJava. It can't be made explicit (as then the method signature in the ViewTemplates subclass conflicts with the superclass, causing a compile error). Can't see an obvious way to get customised views working for v3.0-M4 for Java developers. Happy to be directed to someone using Java who has this going...

sdk451 avatar May 25 '16 05:05 sdk451