segunda-feira, 9 de setembro de 2013

ASP.NET MVC 4 - Upload e Visualização de Imagens

Neste artigo veremos como o processo de manipulação de imagens no ASP.NET MVC se tornou mais simples. Neste tutorial, faremos o seguinte:
1 - Criaremos um projeto ASP.NET MVC 4 e adicionaremos um banco de dados local, via EntityFramework, com uma tabela para armazenar os dados da imagem na qual efetuaremos o Upload
2 - Atualizaremos a interface MVC para efetuar o Upload de um arquivo
3 - Criaremos um mecanismo para visualizar a imagem que foi inserida no nosso banco de dados local

BAIXE OS FONTES (EM .NET CORE): ASP.NET MVC CORE - Image Upload


1 - Projeto com a tabela de Imagens

Crie um novo projeto no VS 2012, conforme imagens abaixo:



Adicione uma classe chamada [Imagem.cs] dentro da pasta [Models] do projeto criado, com o seguinte conteúdo:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;

namespace MvcApplication2.Models
{
    public class Imagem
    {
        public static int _inc;
        public int Id { get; set; }
        public string Nome { get; set; }
        public string Conteudo { get; set; }
        public string Tipo { get; set; }
        public int Tamanho { get; set; }

        public Imagem() {
            Id = Interlocked.Increment(ref _inc);
        }
    }
}

Compile o projeto, e depois adicione uma classe controladora chamada [ImagemController] dentro da pasta [Controllers] do projeto, da forma que vemos nas imagens abaixo:



Neste ponto já temos nosso cadastro pronto. Vamos adicionar o objeto de Upload de imagens nas telas [Create.cshtml e Edit.cshtml].

2 - Atualização da Create.cshtml

Atualize conteúdo da View Create.cshtml por todo código abaixo:


@model MvcApplication2.Models.Imagem

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

<script src="~/Scripts/jquery-1.8.2.js" type="text/javascript"></script>

<script type="text/javascript">

    $(document).ready(function () {
        $("#divFileUpload").mouseenter(function () {
            imageStatusOnHover();
        }).mouseleave(function(){ 
            imageStatusOffHover();
        });

    });

    function imageStatusOnHover() {
        if ($("#file1").val() != "") {
            var fileURL = $("#file1").val().replace(/\\/g, "\\\\");
            showImagemTooltip(fileURL);
        }
    }

    function imageStatusOffHover() {
        $('#divImageTooltip').hide();
    }

    function showImagemTooltip(file) {
        $('#divImageTooltip').css({
            "backgroundImage": "url(" + file + ")",
            "background-repeat": "no-repeat",
            "background-size": "contain",
            "background-position": "center",
            "background-color": "white"
        });

        $('#divImageTooltip').show();
        $("#divFileUpload").mousemove(function (event) {
            $('#divImageTooltip').css({ 'top': event.pageY, 'left': event.pageX });
        });
    }


</script>

@using (Html.BeginForm("Create", "Imagem", FormMethod.Post, new { enctype = "multipart/form-data" })) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Imagem</legend>

        <table>
            <tr>
                <td>
                    <div id="divFileUpload">
                        <input type="file" id="file1" name="file1" />
                    </div>
                </td>
            </tr>
            <tr>
                <td>
                    <div id="divImageTooltip" style="display:none; position: absolute; height:200px; width:200px; border:solid 1px black;"></div>
                </td>                
            </tr>
        </table>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Desta forma, executando a aplicação, adicionando a URL "/Imagem" e clicando no botão [Create New], poderá já testar o recurso de Upload e Visualização de uma imagem [ao passar o mouse sobre o objeto de Upload], conforme mostra a tela abaixo:



Agora precisamos criar um processo para salvar/visualizar a imagem no nosso banco de dados.

3 - Salvar e Visualizar a Imagem do Banco de Dados

Altere todo conteúdo do seu arquivo [Index.cshtml] pelo especificado abaixo:


@model IEnumerable<MvcApplication2.Models.Imagem>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Nome)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Tipo)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Tamanho)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Nome)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Tipo)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Tamanho)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
            @Html.ActionLink("Details", "Details", new { id=item.Id }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.Id })
        </td>
    </tr>
}

</table>


Altere todo o conteúdo do arquivo [Edit.cshtml] pelo conteúdo abaixo:


@model MvcApplication2.Models.Imagem

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

<script src="~/Scripts/jquery-1.8.2.js" type="text/javascript"></script>

<script type="text/javascript">

    $(document).ready(function () {
        $("#divFileUpload").mouseenter(function () {
            imageStatusOnHover();
        }).mouseleave(function(){ 
            imageStatusOffHover();
        });

    });

    function imageStatusOnHover() {
        var fileTitleContent = $("#fileTitle1")[0].textContent;
        if (fileTitleContent != "")
        {
            var url = String(window.location.protocol) + "//" + String(window.location.host) + "/";
            url += "Imagem/BuscarAnexoImagemPorNome?nomeImagem=" + fileTitleContent;

            $.ajax({
                type: "POST",
                url: url,
                success: function (file) {
                    showImagemTooltip(file);
                }
            });
        }
    }

    function imageStatusOffHover() {
        $('#divImageTooltip').hide();
    }

    function showImagemTooltip(file) {
        $('#divImageTooltip').css({
            "backgroundImage": "url(" + file.Url + ")",
            "background-repeat": "no-repeat",
            "background-size": "contain",
            "background-position": "center",
            "background-color": "white"
        });

        $('#divImageTooltip').show();
        $("#divFileUpload").mousemove(function (event) {
            $('#divImageTooltip').css({ 'top': event.pageY, 'left': event.pageX });
        });
    }


</script>

@using (Html.BeginForm("Edit", "Imagem", FormMethod.Post, new { enctype = "multipart/form-data" }))
{

    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Imagem</legend>

        <table>
            <tr>
                <td>
                    <div id="divFileUpload">
                        <label id="fileTitle1">@ViewBag.FileTitleLabel</label>
                        <input type="file" id="file1" name="file1" />
                    </div>
                </td>
            </tr>
            <tr>
                <td>
                    <div id="divImageTooltip" style="display:none; position: absolute; height:200px; width:200px; border:solid 1px black;"></div>
                </td>                
            </tr>
        </table>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}


E finalmente, altere também todo conteúdo da classe [ImagemController.cs] pelo especificado abaixo.
Obs: Como o objetivo é mostrar apenas os recursos de imagem, não me preocupei em separar o projeto em camadas, pois vai notar que o acesso a dados ficou na classe controladora.


using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcApplication2.Models;

namespace MvcApplication2.Controllers
{
    public class ImagemController : Controller
    {
        private StoreDBContext db = new StoreDBContext();

        //
        // GET: /Imagem/

        public ActionResult Index()
        {
            return View(db.Imagems.ToList());
        }

        //
        // GET: /Imagem/Details/5

        public ActionResult Details(int id = 0)
        {
            Imagem imagem = db.Imagems.Find(id);
            if (imagem == null)
            {
                return HttpNotFound();
            }
            return View(imagem);
        }

        //
        // GET: /Imagem/Create

        public ActionResult Create()
        {
            return View();
        }

        //
        // POST: /Imagem/Create

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Imagem imagem)
        {
            var img = new Imagem();
            var file1 = Request.Files["file1"];

            if (!string.IsNullOrEmpty(file1.FileName))
            {
                img.Nome = file1.FileName.Substring(file1.FileName.LastIndexOf("\\") + 1);

                var binaryData = new Byte[file1.InputStream.Length];
                file1.InputStream.Read(binaryData, 0, (int)file1.InputStream.Length);
                var base64String = Convert.ToBase64String(binaryData, 0, binaryData.Length);
                img.Conteudo = base64String;

                img.Tamanho = file1.ContentLength;
                img.Tipo = file1.ContentType;

                db.Imagems.Add(img);
                db.SaveChanges();
            }

            return RedirectToAction("Index");
        }

        //
        // GET: /Imagem/Edit/5

        public ActionResult Edit(int id = 0)
        {
            Imagem imagem = db.Imagems.Find(id);

            ViewBag.FileTitleLabel = "";
            if (imagem != null) ViewBag.FileTitleLabel = imagem.Nome;

            return View(imagem);
        }

        //
        // POST: /Imagem/Edit/5

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(Imagem imagem)
        {

            ViewBag.FileTitleLabel = "";

            return RedirectToAction("Index");
        }

        //
        // GET: /Imagem/Delete/5

        public ActionResult Delete(int id = 0)
        {
            Imagem imagem = db.Imagems.Find(id);
            if (imagem == null)
            {
                return HttpNotFound();
            }
            return View(imagem);
        }

        //
        // POST: /Imagem/Delete/5

        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public ActionResult DeleteConfirmed(int id)
        {
            Imagem imagem = db.Imagems.Find(id);
            db.Imagems.Remove(imagem);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        public ActionResult BuscarAnexoImagemPorNome(string nomeImagem)
        {
            return Json(new { Url = Url.Action("CriarAnexoImagem", "Imagem", new { nomeImagem = nomeImagem }) });
        }

        public FileContentResult CriarAnexoImagem(string nomeImagem)
        {
            var file = db.Imagems.Where(w => w.Nome.Equals(nomeImagem));
            if (file == null) return null;

            return File(Convert.FromBase64String(file.FirstOrDefault().Conteudo), file.FirstOrDefault().Tipo, file.FirstOrDefault().Nome);
        }

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}


Nesta classe controladora, e no arquivo Edit.cshtml, vai notar pontos interessantes:
- Ao passar o mouse sobre o objeto de Upload de arquivos, ocorre uma chamada Ajax para executar o método [Imagem/BuscarAnexoImagemPorNome]
- Este método busca o registro da imagem e retorna um JSON com a chamada para outro método [CriarAnexoImagem] que vai retornar o arquivo (FileContentResult), através da conversão da String que foi salva no formato [Base64] no banco de dados (na Action Create)
- Depois o JScript [showImagemTooltip] mostra a imagem em um Div, tendo como background a imagem retornada.

Em fim, sua tela de consulta deverá estar como exemplificado abaixo:



[]s

terça-feira, 3 de setembro de 2013

ASP.NET MVC 4 - EF Data Migration

A quantidade de recursos para desenvolvimento Web usando o Microsoft ASP.NET MVC 4 é realmente grande. Falando um pouco destes recursos, existe um que surgiu, a partir da versão 4.3 do EntityFramework, que se chama Data Migration.
Com este recurso podemos unir o poder da codificação .NET com Scripts de banco de dados!

Por exemplo, imagine o cenário onde:
1 - Criaremos uma classe que representará o modelo de uma tabela de Produtos e baseado neste modelo usaremos o EntityFramework para gerar um Banco de Dados com esta tabela
2 - Habilitaremos o recurso de Data-Migration no VS 2012
3 - Efetuaremos alterações nesta tabela, usando o recurso Data-Migration
4 - Criaremos um Script de Banco de Dados, através do Data-Migration

Vamos começar:

1 - Criar a classe de Produtos
Abra o VS 2012 e crie um novo projeto ASP.NET MVC 4, com o template [Internet Application], conforme ilustram as figuras abaixo:


Com o projeto criado, adicione una classe chamada Product.cs, dentro do diretório [Models]. Segue conteúdo da classe:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcApplication1.Models
{
    public class Product
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }
}

Importante: Neste ponto COMPILE a solução para que esta classe fique visível ao controladores.
Agora vamos criar nosso Controller do ASP.NET MVC; dentro da pasta [Controllers] adicione um controlador chamado "ProductsController", de acordo com a imagem abaixo: 





Importante: Note a opção [Data Context Class]; Neste momento vamos criar uma NOVA, para justamente começarmos a nos beneficiar do recurso do EntityFramework. Escolha, portanto, a opção [New Data Context]. Uma nova janela aparecerá para nomearmos nosso EF. Renomeie conforme ilustra a imagem abaixo: 




Clique no botão [Ok] depois em [Add] na tela principal para criarmos nosso Controller.
Neste momento teremos na nossa solução do VS toda a estrutura pronta para nosso cadastro de produtos, conforme ilustra a imagem: 



Tanto que neste ponto, podemos executar a aplicação e adicionar a URL do browser "/Products" e ver que temos um cadastro funcional. Mas o que nos interessa vem agora, que é a utilização do Data-Migration para gerarmos um Script de banco de dados da tabela de produtos.

Note que no momento em que você criou a classe StoreDBContext.cs, o VS criou um banco de dados para lhe apoiar neste processo. Veja como localizá-lo:
- Entre no Web.config da solução, procure pela palavra [StoreDBContext] e notará que foi criada uma ConnectionString que aponta para um [Data Source]. No meu caso, está apontando para [(localdb)\v11.0].
- Entre no [Server Explorer] do VS e EXCLUA todos os itens da opção [Data Connections]
- Adicione uma nova conexão configurada conforme a imagem:



Também perceba que escolho um banco de dados já existente, chamado [StoreDBContext-20130903153401]
Com isto, seu server explorer agora mostra a tabela de Produtos que você criou somente como uma Classe no seu projeto:



2 - Habilitar o Data-Migration
Como utilizaremos o VS 2012, já temos a disposição a versão mais nova do EntityFramework, a 4.5, onde usaremos o recurso do Data-Migration. Para tanto, clique no Menu Tools -> Library Package Manager -> Package Manager Console
Com o console visível, digite:

Enable-Migrations -ContextTypeName MvcApplication1.Models.StoreDBContext

Com isto, note que foi criado na solução do VS uma pasta chamada [Migrations] com sua primeira versão do [code-based Migration - 201309031854591_InitialCreate.cs].
Sua console/VS deverá estar como ilustra as imagens:




Depois analise com calma a classe [*_InitialCreate.cs] e perceba que existe um código pronto para suporte de geração de Script de Migração.

3 - Alteração da classe de Produtos
Em um processo real, as alterações no modelo de uma tabela virão com frequência. Portanto, como nos baseamos sempre na alteração da classe que representa a tabela, vamos adicionar uma campo chamado [Price] na classe e alterar o Banco de Dados usando o Data-Migration.
Adicione a seguinte propriedade na classe Products.cs :

   public decimal Price { get; set; }

Salve a classe e digite, no Package Manager Console:

   Add-Migration AddProductsPrice

O nome [AddProductsPrice] é livre, você adiciona o que desejar. Como resultado vai notar que na pasta [Migrations] do VS existirá agora o arquivo [201309031931456_AddProductsPrice.cs] que reflete a alteração do novo campo criado.

Digite agora no console:

Update-Database

E vai notar no [Server Explorer] que o VS alterou a estrutura da tabela de Produtos.

4 - Criar o Script de Banco de Dados pelo Data-Migration
Como processo final vamos gerar o Script, digitando no console:

   Update-Database -Script -SourceMigration: $InitialDatabase

Pronto! O VS criou o Script para ser executado no Sql Server por um DBA.



Abraços!

Refs:
Automatic Code First Migrations
Code First Migrations

quinta-feira, 8 de agosto de 2013

Desabilitando o uso do [Temporary ASP.NET Files] no VS.NET

Qual desenvolvedor já não limpou o diretório de arquivos temporários do .NET Framework algumas centenas de vezes?! 

O VS.NET usa este importante repositório [Microsoft.NET\Framework\versionNumber\Temporary ASP.NET Files] quando você compila sua aplicação.

O principal objetivo disto é a performance na execução da aplicação, pois na primeira compilação do seu código todas as DLLs são copiadas para este TempDir e usadas em subsequentes chamadas, não necessitando novamente de uma compilação pelo VS.NET.

É claro que não existem somente pontos a favor disto pois, por questões de permissão, por exemplo, começemos a nos deparar com erros 'estranhos' no VS.NET.

Talvez já se tenha deparado com o seguinte erro ao tentar executar sua aplicação:



Por padrão, sua aplicação Web usa este TempDir. Mas podemos desabilitar isto, adicionando apenas uma instrução no Web.config da aplicação, conforme ilustrado abaixo:


    <system.web>
      <hostingEnvironment shadowCopyBinAssemblies="false" />
    </system.web>


Portanto, ao configurar a propriedade [shadowCopyBinAssemblies] para [false] estaremos desabilitando o uso deste repositório. É claro que devemos usar com cautela, pois estamos 'prejudicando' a performance da aplicação. Após os testes de sua aplicação, recomenda-se remover esta instrução ao copiar sua aplicação para um servidor de produção.

Ref: Understanding ASP.NET Dynamic Compilation

[]s!

sexta-feira, 19 de julho de 2013

Dicas - Codificando Stored Procedures

Criar Stored Procedures é muito útil em qualquer projeto, pois nos proporciona grande flexibilidade no desenvolvimwento. Durante toda nossa atuação em projetos sempre temos algo a aprender e melhorar. Neste intuito, segue abaixo alguns recursos interessantes que podem ser aplicados a qualquer tipo de projeto, pequeno ou grande.

1 - Uso de Cursor

Em projetos grandes o uso de cursor acaba degradando a performance de sua Stored Procedure. Como alternativa, segue recurso que substitui o uso de cursores.

-- Como exemplo, criaremos uma Tabela Temporária qualquer
CREATE TABLE #tmp
(
 cod int,
 valor nvarchar(max)
)

INSERT INTO #tmp VALUES (1, 'Valor1')
INSERT INTO #tmp VALUES (2, 'Valor2')
INSERT INTO #tmp VALUES (3, 'Valor3')

-- Criamos uma nova temporária que vai contér o [ID] de cada linha, para efetuarmos o LOOP.
SELECT ROW_NUMBER() OVER(ORDER BY cod) AS row, valor
INTO #tmpLoop
FROM #tmp

-- Criamos o processo de Loop
DECLARE @iTmp INT = 1
DECLARE @countTmp INT
SELECT @countTmp = COUNT(row) FROM #tmpLoop

DECLARE @valor NVARCHAR(MAX)

WHILE (@iTmp <= @countTmp)
BEGIN 
 SELECT @valor = valor FROM #tmpLoop WHERE row = @iTmp

 SELECT @valor

 SET @iTmp = @iTmp + 1
END

2 - Retorno de Valores
É útil executar, dentro de sua Stored Procedure, uma outra para efetuar algum processo e retornar um valor. Segue exemplos abaixo.


-- Prepara a variável que receberá o valor e a execução da Proc
DECLARE @Valor NVARCHAR(MAX)
EXEC BuscarValor
  @id = 'passa algum parâmetro para a proc',
  @pValor = @Valor OUTPUT

SELECT @Valor

CREATE PROCEDURE BuscarValor
 @id NVARCHAR(MAX),
 @pValor NVARCHAR(MAX) OUTPUT
AS
BEGIN

 -- Efetua alguma busca e alimenta a variável de retorno [@pValor]
 SET @pValor = 'Algum Valor...'

END

Uma forma interessante para alimentar uma variável (@descricao) a partir da execução de um SQL dinâmico

DECLARE @ParameterDefinition NVARCHAR(MAX) = '@descricao NVARCHAR(MAX) OUTPUT'
DECLARE @SQL NVARCHAR(MAX)
DECLARE @descricao NVARCHAR(MAX)

SET @SQL = ' SELECT TOP 1 @descricao = descricao'
SET @SQL = @SQL + ' FROM TB'
SET @SQL = @SQL + ' WHERE ...'

EXECUTE sp_executesql @SQL,
       @ParameterDefinition,
       @descricao OUTPUT

SELECT @descricao

Ter como retorno uma TABELA também é útil

CREATE TABLE #tmp
(
 nome nvarchar(max),
 valor nvarchar(max)
)

EXEC BuscarTabela
  @id = 1

SELECT * FROM #tmp

CREATE PROCEDURE BuscarTabela
(
 @id INT
)
AS
BEGIN

 INSERT INTO #tmp
  SELECT *
   FROM TB
   WHERE ...

END

3 - Manipulação de XML
Com a função [OPENXML] do Sql Server fica fácil converter um XML em uma tabela.


DECLARE @xml XML, @idoc int

-- Imagine o seguinte XML
SET @xml = '<root>
 <campos>
  <campo campo_id="1" campo_nome="campo1" />
  <campo campo_id="2" campo_nome="campo2" />
  <campo campo_id="3" campo_nome="campo3" />
 </campos>
</root>'

IF OBJECT_ID('tempdb..#xmlTable') IS NOT NULL DROP TABLE #xmlTable
CREATE TABLE #xmlTable
(
 campo_id nvarchar(max), 
 campo_nome nvarchar(max)
)

EXEC sp_xml_preparedocument @idoc OUTPUT, @xml

INSERT INTO #xmlTable
SELECT *
FROM OPENXML (@idoc, '/root/campos/campo', 2)
 WITH (
   campo_id nvarchar(max) '@campo_id', 
   campo_nome nvarchar(max) '@campo_nome'
   );

EXEC sp_xml_removedocument @idoc

SELECT * FROM #xmlTable

4 - PIVOT Table (CrossTab) 
Com o resurso [pivot] do Sql Server a tarefa fica bem mais simples. Analise o exemplo abaixo.

-- Imagine uma [TABELA_ORIGINAL] que contém a estrutura:
-- linha INT
-- campo NVARCHAR(MAX)
-- valor NVARCHAR(MAX)
-- 
-- EXEMPLO:
-- linha campo valor
-- ---------------------------
-- 1  c1 valor1
-- 1  c2 valor2
-- 2  c1 valor3
-- 2  c2 valor4
--
-- E deseja converter os valores do campo [campo] em COLUNAS
-- Execute a Proc abaixo para montar o SQL que realizará esta tarefa
-- e terá como resultado
--
-- linha c1 c2
-- ---------------------------
-- 1  valor1 valor2
-- 2  valor3 valor4

IF OBJECT_ID('tempdb..#tmpCampos') IS NOT NULL DROP TABLE #tmpCampos

CREATE TABLE #tmpCampos
(
 row int,
 campo nvarchar(max)
)

DECLARE @Insert NVARCHAR(MAX)
SET @Insert = 'INSERT INTO #tmpCampos'
SET @Insert = @Insert + ' SELECT ROW_NUMBER() OVER(ORDER BY campo) row, campo'
SET @Insert = @Insert + ' FROM TABELA_ORIGINAL'
SET @Insert = @Insert + ' GROUP BY campo'
SET @Insert = @Insert + ' ORDER BY campo'
EXEC(@Insert)
 
DECLARE @cols AS NVARCHAR(max), @campos AS NVARCHAR(max), @icampos INT
SET @icampos = (SELECT min(row) FROM #tmpCampos)
 
--Variável para conter a relação de campos
SET @cols = N''
--Montagem da relação de campos
WHILE (@icampos is not null)
BEGIN
 SELECT @campos = campo FROM #tmpCampos WHERE row = @icampos
 SET @cols = @cols+N',[' + @campos + N']'
 SET @icampos = (SELECT min(row) FROM #tmpCampos WHERE row > @icampos)
END

SET @cols = substring(@cols,2,len(@cols))

DECLARE @SQL NVARCHAR(MAX)
SET @SQL = N'SELECT *'
SET @SQL = @SQL + N' FROM (SELECT linha,campo,valor FROM TABELA_ORIGINAL) AS p'
SET @SQL = @SQL + N' PIVOT (MAX(valor) FOR campo IN (' + @cols + N')) AS pv'

That's It.

terça-feira, 2 de julho de 2013

Visualizar relatório (Reporting Services) em ASP.NET MVC

Sabemos que é possível distribuir de forma amigável para o usuário final a visualização de um relatório desenvolvido em Reporting Services como parte de sua aplicação. Porém, para quem está desenvolvendo em ASP.NET MVC, parece não existir uma solução tão trivial, comparada a forma ASPX tradicional.

É de conhecimento dos desenvolvedores o uso da biblioteca [Microsoft.ReportViewer.WebForms.dll] para visualização do relatório em suas páginas, mas para MVC parece não existir [ainda] tal biblioteca!... como resolver isto então? Vamos juntar as duas tecnologias!

Para começar, adicione a biblioteca citada no seu projeto ASP.NET MVC:
- Para o Visual Studio 2010 - \Program Files (x86)\Microsoft Visual Studio 10.0\ReportViewer\
- Para o Visual Studio 2012 - \Program Files (x86)\Microsoft Visual Studio 11.0\ReportViewer\

Depois trabalhe de forma a juntar as tecnologias MVC, com uso do Razor .cshtml/.vbhtml e uma página padrão .aspx, da seguinte maneira:

1 - Crie sua página de PrintPreview.cshtml, tendo como conteúdo:

<script language="javascript" type="text/javascript">

    $(document).ready(function () {

        ResizePrintPreview();
    });

    $(window).resize
    (
        function () { ResizePrintPreview(); }
    );

    function ResizePrintPreview() {
        var windowHeight = $(window).height();
        $('#iframe_printpreview').height(windowHeight);
    }

</script>

<iframe id="iframe_printpreview" src="@ViewBag.srcPrintPreview" width="100%">
</iframe>

Como pontos de atenção temos:
- Duas funções JQuery que coloquei apenas para formatar melhor um IFrame na página
- $(document).ready
- $(window).resize

E o IFrame [iframe_printpreview] que vai mostrar nosso relatório do Reporting Services, através do src [src="@ViewBag.srcPrintPreview"]

2 - Sua Index.cshtml
Como toda aplicação MVC, altere sua Index.cshtml, para contér o seguinte script:

<script src="@Url.Content("~/Scripts/jquery/jquery-1.7.2.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/Relatorio/Relatorio.js")" type="text/javascript"></script>

Isto é necessário para ativar o Script do JQuery e sua biblioteca JS - neste exemplo, Relatorio.js .

3 - Sua biblioteca Relatorio.js

function executarRelatorio(urlRS) {
    var location = String(window.location.protocol) + "//" + String(window.location.host) + "/";
    
    var urlPreview = "/PrintPreview";
    urlPreview += "?urlRS=" + urlRS;     
    urlPreview += "&Location=" + location;

    var sFeatures = "height=600, width=1100, resizable=yes, scroll=yes, status=no";
    window.open(urlPreview, null, sFeatures);
}

O que contém de interesante na função [executarRelatorio]
- A passagem de parâmetro [urlRS] que conterá o caminho do seu relatório do servidor do RS, por exemplo:
http://rsServer/reportserver/Desenv/NomedoMeuRelatorio

- A variável [urlPreview] - vai contér o caminho da função que será executada dentro do CONTROLER principal do seu projeto ASP.NET MVC.

- Os parâmetros que serão passados para a classe PrintPreview do controlador principal do seu projeto.

4 - A função do seu CONTROLADOR

        public ActionResult PrintPreview(string urlRS, string Location)
        {
            var rsName = "reportserver";
            var rsLeng = rsName.Length;
            var pos = urlRS.IndexOf(rsName) + rsLeng;
            var ReportUrl = urlRS.Substring(0, pos);
            var ReportPath = urlRS.Substring(pos, urlRS.Length- pos);

            Session["ReportUrl"] = ReportUrl;
            Session["ReportPath"] = ReportPath;

            ViewBag.srcPrintPreview = Location + "DIRETORIO_ASPX/PrintPreview.aspx";
            
            return View("PrintPreview");
        }

Note a preparação da [ViewBag.srcPrintPreview] usada no passo 1 deste artigo, que finalmente vai mostrar na página ASPX seu relatório do Reporting Services.

Note também que, no lugar do diretório [DIRETORIO_ASPX] que coloquei como exemplo você precisa substituir por um diretório válido.

Importante: Este diretório não pode estar [dentro] da estrutura padrão MVC, pois não é possivel ao mesmo tempo mesclar dentro desta esttrutura as arquiteturas MVC e ASPX padrão. Portanto, crie este diretório fora da estrutura:

PROJETO_MVC\VIEWS\

5 - Faltou agora definir a página PrintPreview.aspx e seu Code-Behind

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="PrintPreview.aspx.cs" Inherits="Relatorios.PrintPreview" ValidateRequest="false" %>
<%@ Register Assembly="Microsoft.ReportViewer.WebForms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" Namespace="Microsoft.Reporting.WebForms" TagPrefix="rsweb" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>

        <rsweb:ReportViewer ID="rswebPrintPreview" runat="server" Width="100%" ProcessingMode="Remote">
        </rsweb:ReportViewer>

    </div>
    </form>
</body>
</html>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Configuration;
using Microsoft.Reporting.WebForms;
using System.Web.Helpers;

namespace Relatorios
{
    public class ReportCredentials : Microsoft.Reporting.WebForms.IReportServerCredentials
    {
        string _userName, _password, _domain;

        public ReportCredentials(string userName, string password, string domain)
        {
            _userName = userName;
            _password = password;
            _domain = domain;
        }

        public System.Security.Principal.WindowsIdentity ImpersonationUser
        {
            get
            {
                return null;
            }
        }

        public System.Net.ICredentials NetworkCredentials
        {
            get
            {
                return new System.Net.NetworkCredential(_userName, _password, _domain);
            }
        }

        public bool GetFormsCredentials(out System.Net.Cookie authCoki, out string userName, out string password, out string authority)
        {
            userName = _userName;
            password = _password;
            authority = _domain;
            authCoki = new System.Net.Cookie(".ASPXAUTH", ".ASPXAUTH", "/", _domain);
            return true;
        }
    }

    public partial class PrintPreview : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (this.IsPostBack) return;

            string usr = "reportUserName";
            string pwd = "reportPassword";
            string domain = "reportDomain";

            string ReportUrl = Session["ReportUrl"].ToString();
            string ReportPath = Session["ReportPath"].ToString();

            rswebPrintPreview.ServerReport.ReportServerCredentials = new ReportCredentials(usr, pwd, domain);
            rswebPrintPreview.ServerReport.ReportServerUrl = new Uri(ReportUrl);
            rswebPrintPreview.ServerReport.ReportPath = String.Format(ReportPath);
        }
    }
}

Portanto, em resumo, faremos o seguinte:
- Criaremos uma PartialView chamada PrintPreview.cshtml;
- Adicionaremos os scripts necessários na Index.cshtml;
- Criaremos a biblioteca Relatorio.js, com a função que será chamada, por exemplo, ao clicar em um Botão dentro de sua Index.cshtml;
- Adicionaremos a função PrintPreview dentro do seu CONTROLADOR do ASP.NET MVC;
- Criaremos a página PrintPreview.aspx e seu Code-Behind

Abraços!