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!

quarta-feira, 26 de junho de 2013

Depurando Serviços WCF com Fiddler

Existem projetos em WCF, que por sua natureza complexa, fica difícil depurar certos erros.

Por exemplo, imagine uma estrutura de projeto que comece com a execução de uma aplicação ASP.NET. Esta aplicação, por sua vez, possui em suas páginas chamadas Silverlight. Dentro desta camada Silverlight, existe uma outra chamada WCF, que executa a tecnologia EntityFramework para busca de informações em um banco de dados qualquer. Estrutura um pouco complicada não?!

Pode ter certeza que em algum momento, ocorrendo um erro lá na última camada do projeto (EntityFramework), o erro que irá aparecer para o desenvolvedor com certeza não será tão detalhado na primeira camada (ASP.NET).

Felizmente existe uma ferramenta muito boa para depurarmos melhor todo este processo, que se chama Fiddler.

A instalação e utilização desta ferramenta de [captura de chamadas WCF] para analisar em muito mais detalhes o erro ocorrido em sua aplicação é simples, como descrito abaixo:

1 - Efetue o download da ferramenta, acessando o site da Fiddler
2 - Instale a ferramenta (fiddler4setup.exe);
3 - Efetue o download e instale o extensor (BinaryMessageFiddlerExtension_0.2.zip) para chamadas WCF - WCF Binary-encoded Message Inspector for Fiddler
4 - Descompacte o arquivo [BinaryMessageFiddlerExtension_0.2.zip] e copie a dll [BinaryMessageFiddlerExtension.dll] para o diretório de instalação do Fiddler (\Program Files\Fiddler2\Inspectors)
5 - E por fim inicie o Fiddler



Com o fiddler iniciado, vá até a opção de Menu [Tools \ Fiddler Options] e configure qual a PORTA que deseja que o fiddler fique "escutando" solicitações HTTP.



Pronto. Quando você iniciar sua aplicação, verá que o fiddler (na aba INSPECTOR) mostrará os resultados das solicitações para você. Desta forma, poderá ver (na aba Inspectors \ TextView) a mensagem de erro em detalhes que precisa depurar.


Ref: Debugging WCF Service Calls


Abraços!

quinta-feira, 23 de maio de 2013

CRM IFD - Habilitar Exportação de Planilha Dinâmica

Existem alterações interessantes que podem ser feitas no Metadados do CRM - infelizmente não especificadas no SDK do próprio produto - como por exemplo, habilitar o recurso de exportação de planilha dinâmica do excel no CRM para IFD.
Por padrão, o CRM cria no excel uma [connection string] que aponta diretamente para o Servidor de Banco de Dados do CRM para buscar os dados. Porém, via IFD, esta conexão não é mais válida, pois você pode estar fora do domínio onde o servidor de banco do CRM se encontra.

Pois bem, para habilitar este recurso no IFD, faça o seguinte:

CRM 4.0
- Entre no servidor de aplicação do CRM;
- Abra o Regedit
- Vá até HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSCRM
- E crie uma [DWORD Value] chamada UseWebQueryForLiveExport com o valor 1

CRM 2011
- Entre no servidor de aplicação do CRM;
- Abra o console do PowerShell do Windows (Iniciar => Todos os Programas => Acessórios => Windows PowerShell)
- Digite:
- Add-PSSnapin Microsoft.Crm.PowerShell
- $setting = New-Object "Microsoft.Xrm.Sdk.Deployment.ConfigurationEntity"
- $setting.LogicalName = "Organization"
- $setting.Attributes = New-Object "Microsoft.Xrm.Sdk.Deployment.AttributeCollection"
- $keypair = New-Object "System.Collections.Generic.KeyValuePair[String,Object]" ("UseWebQueryForLiveExport",$True)
- $setting.Attributes.Add($keypair)
- Set-CrmAdvancedSetting -Entity $setting

Fontes:
- Microsoft Office Excel worksheets exported from Microsoft Dynamics CRM are slow to open
- Organization Table Metadata (Advanced Settings)

[]s