08 View e Ajax

Compartilhando objetos com a view

Para registrar objetos a serem acessados na view, usamos o método include:

@Resource
class ClientController {
    private final Result result;
    public ClientController(Result result) {
        this.result = result;
    }

    public void busca(int id) {
        result.include("mensagem", "Alguma mensagem");
        result.include("cliente", new Cliente(id));
    }
}

Agora as variáveis "mensagem" e "cliente" estão disponíveis para uso em seu template engine. É possível registrar o objeto por meio da invocação do método include com um único argumento:

@Resource
class ClientController {
    private final Result result;
    public ClientController(Result result) {
        this.result = result;
    }

    public void busca(int id) {
        result.include("Alguma mensagem").include(new Cliente(id));
    }
}

Nesse caso, a primeira invocação registra a chave "string" e a segunda, a chave "cliente". Você pode alterar o comportamento de convenção de chaves no seu próprio TypeNameExtractor.

Custom PathResolver

Por padrão, para renderizar suas views, o VRaptor segue a convenção:

public class ClientsController {
    public void list() {
        //...
    }
}

Este método acima renderizará a view /WEB-INF/jsp/clients/list.jsp. No entanto, nem sempre queremos esse comportamento, e precisamos usar algum template engine, como por exemplo, Freemarker ou Velocity, e precisamos mudar essa convenção. Um jeito fácil de mudar essa convenção é estendendo a classe DefaultPathResolver:

@Component
public class FreemarkerPathResolver extends DefaultPathResolver {
    protected String getPrefix() {
        return "/WEB-INF/freemarker/";
    }

    protected String getExtension() {
        return "ftl";
    }
}

Desse jeito, a lógica irá renderizar a view /WEB-INF/freemarker/clients/list.ftl. Se, ainda assim, isso não for o suficiente, você pode implementar a interface PathResolver e fazer qualquer convenção que você queira, não esquecendo de anotar a classe com @Component.

View

Se você quiser mudar a view de alguma lógica específica, você pode usar o objeto Result:

@Resource
public class ClientsController {

    private final Result result;

    public ClientsController(Result result) {
        this.result = result;
    }

    public void list() {}

    public void save(Client client) {
        //...
        this.result.use(Results.logic()).redirectTo(ClientsController.class).list();
    }
}

Por padrão, existem estes tipos de views implementadas:

Atalhos no Result

Alguns redirecionamentos são bastante utilizados, então foram criados atalhos para eles. Os atalhos disponíveis são:

Além disso, se o redirecionamento é para um método do mesmo Controller, podemos usar:

Redirecionamento e forward

No VRaptor3, podemos tanto realizar um redirect ou um forward do usuário para uma outra lógica ou um JSP. Apesar de serem conceitos da API de Servlets, vale a pena relembrar a diferença: o redirecionamento acontece no lado do cliente, através de códigos HTTP que farão o browser acessar uma nova URL; já o forward acontece no lado do servidor, totalmente transparente para o cliente/browser.

Um bom exemplo de uso do redirect é no chamado 'redirect-after-post'. Por exemplo: quando você adiciona um cliente e que, após o formulário submetido, o cliente seja retornado para a página de listagem de clientes. Fazendo isso com redirect, impedimos que o usuário atualize a página (F5) e reenvie toda a requisição, acarretando em dados duplicados.

No caso do forward, um exemplo de uso é quando você possui uma validação e essa validação falhou, geralmente você quer que o usuário continue na mesma tela do formulário com os dados da requisição preenchidos, mas internamente você vai fazer o forward para outra lógica de negócios (a que prepara os dados necessários para o formulário).

Escopo Flash automático

Se você adicionar objetos no Result e fizer um Redirect, esses objetos estarão disponíveis na próxima requisição.
public void adiciona(Cliente cliente) {
    dao.adiciona(cliente);
    result.include("mensagem", "Cliente adicionado com sucesso");
    result.redirectTo(ClientesController.class).lista();
}
lista.jsp:
...
<div id="mensagem">
   <h3>${mensagem}</h3>
</div>
...

Accepts e o parâmetro _format

Muitas vezes, precisamos renderizar formatos diferentes para uma mesma lógica. Por exemplo queremos retornar um JSON, em vez de um HTML. Para fazer isso, podemos definir o Header Accepts da requisição para que aceite o tipo desejado, ou colocar um parâmetro _format na requisição.

Se o formato for JSON, a view renderizada por padrão será: /WEB-INF/jsp/{controller}/{logic}.json.jsp, ou seja, em geral será renderizada a view: /WEB-INF/jsp/{controller}/{logic}.{formato}.jsp. Se o formato for HTML você não precisa colocá-lo no nome do arquivo. O parâmetro _format tem prioridade sobre o header Accepts.

Ajax: construindo na view

Para devolver um JSON na sua view, basta que sua lógica disponibilize o objeto para a view, e dentro da view você forme o JSON como desejar. Como no exemplo, o seu /WEB-INF/jsp/clients/load.json.jsp:

{ nome: '${client.name}', id: '${client.id}' }

E na lógica:

@Resource
public class ClientsController {

    private final Result result;
    private final ClientDao dao;

    public ClientsController(Result result, ClientDao dao) {
        this.result = result;
        this.dao = dao;
    }

    public void load(Client client) {
        result.include("client", dao.load(client));
    }
}

Ajax: Versão programática

Se você quiser que o VRaptor serialize automaticamente seus objetos para XML ou JSON, você pode escrever em sua lógica:

import static br.com.caelum.vraptor.view.Results.*;
@Resource
public class ClientsController {

    private final Result result;
    private final ClientDao dao;

    public ClientsController(Result result, ClientDao dao) {
        this.result = result;
        this.dao = dao;
    }

    public void loadJson(Cliente cliente) {
        result.use(json()).from(cliente).serialize();
    }
    public void loadXml(Cliente cliente) {
        result.use(xml()).from(cliente).serialize();
    }
}

Os resultados vão ser parecidos com:

{"cliente": {
    "nome": "Joao"
}}
<cliente>
    <nome>Joao</nome>
</cliente>

Por padrão, apenas campos de tipos primitivos serão serializados (String, números, enums, datas), se você quiser incluir um campo de tipo não primitivo você precisa incluí-lo explicitamente:

result.use(json()).from(cliente).include("endereco").serialize();

vai resultar em algo parecido com:

{"cliente": {
    "nome": "Joao",
    "endereco" {
        "rua": "Vergueiro"
    }
}}

Você pode também excluir os campos de tipo primitivo da serialização:

result.use(json()).from(usuario).exclude("senha").serialize();

vai resultar em algo parecido com:

{"usuario": {
    "nome": "Joao",
    "login": "joao"
}}

Ou ainda você pode serializar recursivamente (cuidado com ciclos):

result.use(json()).from(usuario).recursive().serialize();
result.use(xml()).from(usuario).recursive().serialize();

A implementação padrão é baseada no XStream, então é possível configurar a serialização por anotações ou configurações diretas ao XStream, bastando criar a classe:

@Component
public class CustomXMLSerialization extends XStreamXMLSerialization {
//or public class CustomJSONSerialization extends XStreamJSONSerialization {
    //delegate constructor

    @Override
    protected XStream getXStream() {
        XStream xStream = super.getXStream();
        //suas configurações ao XStream aqui
        return xStream;
    }
}

Serializando Collections

Ao serializar coleções, o padrão é colocar a tag "list" em volta dos elementos:

List<Cliente> clientes = ...;
result.use(json()).from(clientes).serialize();
//ou
result.use(xml()).from(clientes).serialize();

vai resultar em algo como:

{"list": [
    {
        "nome": "Joao"
    },
    {
        "nome": "Maria"
    }
]}

ou

<list>
    <cliente>
        <nome>Joao</nome>
    </cliente>
    <cliente>
        <nome>Maria</nome>
    </cliente>
</list>

É possível personalizar o elemento de fora usando o método:

List<Cliente> clientes = ...;
result.use(json()).from(clientes, "clientes").serialize();
//ou
result.use(xml()).from(clientes, "clientes").serialize();

vai resultar em algo como:

{"clientes": [
    {
        "nome": "Joao"
    },
    {
        "nome": "Maria"
    }
]}

ou

<clientes>
    <cliente>
        <nome>Joao</nome>
    </cliente>
    <cliente>
        <nome>Maria</nome>
    </cliente>
</clientes>

Os includes e excludes funcionam como se você os estivesse aplicando num elemento de dentro da lista. Por exemplo se você quiser incluir o endereço no cliente:

List<Cliente> clientes = ...;
result.use(json()).from(clientes).include("endereco").serialize();

com resultado:

{"list": [
    {
        "nome": "Joao",
        "endereco": {
            "rua": "Vergueiro, 3185"
        }
    },
    {
        "nome": "Maria",
        "endereco": {
            "rua": "Vergueiro, 3185"
        }
    }
]}

Serializando JSON sem elemento raiz

Se você quiser serializar um objeto em JSON sem dar nomes a eles, pode fazer isso com o método withoutRoot:

result.use(json()).from(carro).serialize(); //=> {'carro': {'cor': 'azul'}}
result.use(json()).withoutRoot().from(carro).serialize(); //=> {'cor': 'azul'}