Imagine que você tenha um relatório de dashboard no seu sistema e que você queira que esse relatório seja automaticamente atualizado de tempos em tempos. Pode ser que esse relatório fique 100% do tempo aberto em uma tela separada e que ele seja crucial para detectar irregularidades no processo. E aí, será que é possível atualizarmos um relatório do Report Viewer de tempos em tempos? É sim! Só precisamos utilizar um Timer. E isso é o que eu vou mostrar para você no artigo de hoje.
Criando o projeto de exemplo
Primeiramente, vamos criar um projeto para demonstrarmos essa funcionalidade. Para facilitar, criaremos um projeto do tipo “Windows Forms Application“, porém, saiba que podemos utilizar o Report Viewer no WPF, no WebForms e até mesmo no MVC. A mesma ideia que eu vou apresentar neste artigo para o Windows Forms se aplicaria para qualquer plataforma que você escolher.
Uma vez criado o projeto, vamos adicionar um novo DataSet tipado que servirá de fonte de dados para o relatório que iremos criar. Dê o nome de “DataSetDashboard” para esse novo DataSet e, dentro dele, adicione uma DataTable chamada “Dashboard“, contendo duas colunas (“NomeProduto” e “ValorVendido” – sendo que a coluna “ValorVendido” deve ser do tipo “Decimal“):
Em seguida, vamos criar um relatório muito simples com um gráfico de vendas por produto. Dê o nome de “RelatorioDashboard” para o novo relatório, adicione um gráfico e configure-o de forma que ele fique parecido com a imagem abaixo:
Com o relatório criado, agora chegou a hora de exibi-lo no formulário. Para isso, arraste um controle do Report Viewer para dentro do formulário e configure-o para que ele exiba o relatório que acabamos de criar. Em seguida, no code-behind, vamos criar um método para adicionarmos informações aleatórias no DataSet do relatório:
// C#
Random _rnd = new Random();
private void AdicionarInformacaoAleatoria()
{
var produtos = new string[] { "Produto 1", "Produto 2", "Produto 3" };
var produto = produtos[_rnd.Next(3)];
var valor = (decimal)_rnd.Next(10000) / (decimal)10;
DataSetDashboard.Dashboard.AddDashboardRow(produto, valor);
}
' VB.NET
Private Rnd As New Random
Private Sub AdicionarInformacaoAleatoria()
Dim Produtos = New String() {"Produto 1", "Produto 2", "Produto 3"}
Dim Produto = Produtos(Rnd.Next(3))
Dim Valor = CDec(Rnd.Next(10000)) / CDec(10)
DataSetDashboard.Dashboard.AddDashboardRow(Produto, Valor)
End Sub
Obviamente, esse método não seria necessário em um sistema “de verdade“. Nesse caso, você teria que carregar os dados do banco para dentro do DataSet. De qualquer forma, vamos utilizar esse método de geração de dados aleatórios para termos um conjunto de dados de exemplo neste artigo.
Em seguida, vamos chamar esse método quatro vezes no construtor do formulário para gerarmos alguns dados que serão exibidos pelo relatório:
' VB.NET
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
AdicionarInformacaoAleatoria()
AdicionarInformacaoAleatoria()
AdicionarInformacaoAleatoria()
AdicionarInformacaoAleatoria()
End Sub
Execute o projeto e veja que um gráfico com dados aleatórios será exibido:
Atualizando o relatório de tempos em tempos
Agora que já temos o nosso relatório criado e sendo exibido no formulário, vamos conferir como podemos atualizá-lo de tempos em tempos. Por questões didáticas, vamos fazer uma atualização a cada um segundo. Pelos testes que eu fiz, o controle do Report Viewer não apresenta problemas de memória mesmo se atualizado em intervalos muito pequenos. Porém, eu recomendo que você evite utilizar intervalos muito baixos para não prejudicar a experiência do usuário (afinal de contas, uma tela piscando a cada segundo para atualizar os dados do relatório não é nem um pouco amigável.
Para atualizarmos o relatório de tempos em tempos, temos que utilizar um timer. Vamos declarar o timer no nível do formulário e vamos configurá-lo diretamente no construtor, setando o intervalo desejado (mil milisegundos, ou seja, um segundo) e ajustando o hook para o evento “Tick“:
' VB.NET
Private Timer As Timer
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
AdicionarInformacaoAleatoria()
AdicionarInformacaoAleatoria()
AdicionarInformacaoAleatoria()
AdicionarInformacaoAleatoria()
Timer.Interval = 1000
AddHandler Timer.Tick, AddressOf Timer_Tick
End Sub
Private Sub Timer_Tick(sender As Object, e As EventArgs)
End Sub
Dentro do handler do evento “Tick“, teríamos que colocar o código para recarregar o DataSet e atualizar o relatório. No nosso exemplo, para conseguirmos sentir que os dados estão realmente sendo alterados, nós vamos adicionar mais um dado aleatório no DataSet a cada tick do Timer:
' VB.NET
Private Sub Timer_Tick(sender As Object, e As EventArgs)
AdicionarInformacaoAleatoria()
Me.ReportViewer1.RefreshReport()
End Sub
Feito isso, a única coisa que está faltando fazer é iniciarmos o Timer através do seu método “Start“. Faremos isso no evento “Load” do formulário, logo após o primeiro “Refresh” do relatório:
' VB.NET
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.ReportViewer1.RefreshReport()
Timer.Start()
End Sub
Execute o projeto e veja só que legal fica o relatórios sendo atualizado a cada segundo:
Escondendo a barra de ferramentas
Muito provavelmente esse tipo de atualização automática só será útil para relatórios de dashboard, que serão exibidos em uma tela o tempo todo. Nesses casos, não faz muito sentido que a barra de ferramentas do Report Viewer fique visível. Para ocultá-la, basta configurarmos a propriedade “ShowToolBar” como “False“:
Finalizando o temporizador quando o formulário é fechado
Uma coisa importante que não devemos esquecer é finalizarmos o Timer no momento do fechamento do formulário. Caso contrário, pode ser que o Timer dispare quando o formulário já está destruído, causando uma exception na aplicação. Para isso, vamos fazer um “override” do método “OnClosing” do formulário e vamos finalizar o Timer através do seu método “Stop“:
' VB.NET
Protected Overrides Sub OnClosing(e As CancelEventArgs)
Timer.Stop()
MyBase.OnClosing(e)
End Sub
Concluindo
A atualização automática de relatórios do Report Viewer de tempos em tempos pode ser útil em diversos cenários. O mais comum deles é quando queremos deixar um relatório de dashboard rodando em uma tela separada. No artigo de hoje você viu como utilizar um Timer para implementar essa atualização automática.
Você já precisou deixar um relatório rodando em uma tela dedicada? Caso positivo, você implementou algum mecanismo de atualização automática? Como você fez para atualizar o relatório de tempos em tempos? Deixe os detalhes na caixa de comentários abaixo.
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Você que utiliza o DataGridView para entrada de dados no seu sistema Windows Forms sem necessariamente estar ligado a algum banco de dados, já pensou em imprimir as informações do grid? Uma opção é imprimir o controle em si, mas, muito provavelmente essa não é a opção que traz uma melhor experiência para o usuário. Que tal imprimir o conteúdo do DataGridView no Crystal Reports? Se essa ideia te interessou, continue lendo esse artigo para conferir como implementar essa funcionalidade.
Criando o formulário com DataGridView
Primeiramente, vamos começar criando um novo projeto do tipo “Windows Forms Application“. Na verdade, poderíamos utilizar a mesma lógica deste artigo em outros tipos de projeto (como WPF, Web Forms ou MVC). Só escolhi o Windows Forms por ser mais fácil de demonstrar e por ser o mais conhecido no mercado.
Uma vez criado o projeto, vamos ajustar o formulário, adicionando um DataGridView e algumas colunas (com os nomes “FuncionarioID“, “Nome” e “Sobrenome“). Além disso, vamos colocar um botão logo abaixo do grid que servirá para chamarmos o outro formulário que apresentará o relatório:
Após esse ponto, se executarmos a aplicação, veremos que o grid está funcional, ou seja, podemos adicionar algumas linhas:
E agora? Como criar o relatório?
Uma vez estando o DataGridView já preparado e funcional, vamos criar um novo relatório do Crystal Reports. Dê o nome de “RelatorioFuncionario” para o novo relatório que será criado e escolha a opção de gerar o relatório em branco:
Agora é que surge o problema: como é que escolhemos uma fonte de dados para o relatório? Se olharmos na janela “Database Expert” do Crystal Reports, não encontraremos nenhum DataSet ou classe de dados para utilizarmos no nosso relatório:
E agora? Como desenhamos o relatório sem termos um DataSet ou classe de dados no nosso projeto? A verdade é que é possível gerar a definição de campos manualmente no Crystal Reports através de arquivos TTX, porém, eu não recomendo. A razão para isso é que, mesmo que você não utilize um DataSet ou classe para fazer o design do relatório, você obrigatoriamente terá que passar um DataSet ou coleção de objetos para alimentar o relatório do Crystal Reports (essas são as duas únicas opções de passar dados para o Crystal Reports através de aplicações .NET). Dessa forma, já que teremos que criar um DataSet ou classe para passar os dados para o Crystal Reports, compensa muito mais utilizá-los na hora da definição do relatório.
Dessa forma, vamos ver a seguir como criar um DataSet e como criar uma classe para desenharmos o relatório e alimentarmos com as linhas cadastradas no grid.
Criando o relatório com um DataSet
Primeiramente, vamos ver como podemos criar um DataSet com as informações cadastradas no grid. Já que vamos criar um DataSet, vamos cria-lo como DataSet tipado. Para isso, adicione um novo item ao projeto utilizando o tipo “DataSet” (que está localizado dentro da categoria “Data“). Dê o nome de “DataSetRelatorio” para o novo DataSet e adicione uma nova tabela chamada “Funcionario“:
Em seguida, se voltarmos para a tela de “Database Expert” no Crystal Reports, veremos que o DataSet estará disponível para a utilização no relatório:
Adicione essa tabela ao relatório e trabalhe no design dele de forma que ele fique parecido com a imagem abaixo:
OK. Agora que já temos o relatório desenhado, vamos criar um novo formulário para exibi-lo. Para isso, adicione um novo formulário no projeto, dando o nome de “FormRelatorio“. Dentro desse formulário, adicione um controle do Crystal Reports e selecione o relatório que acabamos de desenhar:
Logo em seguida, vamos até o code-behind do formulário para adicionarmos um novo construtor recebendo o DataSet que alimentará o relatório:
' VB.NET
Public Sub New()
' This call is required by the designer.
InitializeComponent()
End Sub
Public Sub New(DataSet As DataSet)
Me.New()
RelatorioFuncionario1.SetDataSource(DataSet)
CrystalReportViewer1.RefreshReport()
End Sub
Feito isso, agora podemos voltar para o nosso formulário inicial para implementarmos a ação do botão “Relatório“. Como temos que passar um DataSet para o formulário de relatórios, teremos que fazer um “foreach” nas linhas do grid para alimentarmos uma instância do DataSet que criamos anteriormente:
// C#
//Opção 1 - Criando DataSet "na mão":
var dataSet = new DataSetRelatorio();
foreach (DataGridViewRow linha in dataGridView1.Rows)
{
if (!linha.IsNewRow)
{
var novaLinhaDataSet = dataSet.Funcionario.NewFuncionarioRow();
novaLinhaDataSet.FuncionarioID = linha.Cells["FuncionarioID"].Value.ToString();
if (linha.Cells["Nome"].Value != null)
novaLinhaDataSet.Nome = linha.Cells["Nome"].Value.ToString();
if (linha.Cells["Sobrenome"].Value != null)
novaLinhaDataSet.Sobrenome = linha.Cells["Sobrenome"].Value.ToString();
dataSet.Funcionario.AddFuncionarioRow(novaLinhaDataSet);
}
}
var formRelatorio = new FormRelatorio(dataSet);
formRelatorio.Show();
' VB.NET
' Opção 1 - Criando DataSet "na mão"
Dim DataSet As New DataSetRelatorio()
For Each Linha As DataGridViewRow In dataGridView1.Rows
If Not Linha.IsNewRow Then
Dim NovaLinhaDataSet = DataSet.Funcionario.NewFuncionarioRow()
NovaLinhaDataSet.FuncionarioID = Linha.Cells("FuncionarioID").Value.ToString()
If Linha.Cells("Nome").Value <> Nothing Then
NovaLinhaDataSet.Nome = Linha.Cells("Nome").Value.ToString()
End If
If Linha.Cells("Sobrenome").Value <> Nothing Then
NovaLinhaDataSet.Sobrenome = Linha.Cells("Sobrenome").Value.ToString()
End If
DataSet.Funcionario.AddFuncionarioRow(NovaLinhaDataSet)
End If
Next
Dim FormRelatorio As New FormRelatorio(DataSet)
FormRelatorio.Show()
Execute a aplicação, adicione algumas linhas no grid, clique no botão “Relatório” e veja que os dados digitados no grid serão passados para o relatório, justamente como esperado:
Nota: se você receber uma “FileNotFoundException” vindo do controle do Crystal Reports ao executar o projeto, isso quer dizer que falta uma configuração no seu arquivo app.config. Eu mostrei como fazer essa configuração no artigo: Trabalhando com o Crystal Reports no WPF.
Apesar dessa metodologia ter funcionado, ela não é a mais indicada. Como temos que criar o DataSet de qualquer maneira para alimentarmos o relatório, seria muito mais vantajoso fazermos o databinding do DataSet com o grid (ao invés de trabalhar sem databinding e ter que criar o DataSet “na mão” antes de exibir o relatório).
O processo de “bindar” o DataSet com o grid é muito simples. Basicamente, nós temos que criar uma nova instância do DataSet no nível do formulário e, no construtor, temos que utilizar essa instância como “DataSource” do DataGridView. Não podemos esquecer de configurarmos a propriedade “AutoGenerateColumns” como “false” (uma vez que nós já criamos as colunas manualmente no DataGridView) e também temos que configurar a propriedade “DataMember” como “Funcionario” (que é o nome da DataTable):
// C#
public partial class FormFuncionarios : Form
{
DataSetRelatorio _dataSet = new DataSetRelatorio();
public FormFuncionarios()
{
InitializeComponent();
// Opção 2 - DataSet "bindado" no DataGridView
dataGridView1.AutoGenerateColumns = false;
dataGridView1.DataSource = _dataSet;
dataGridView1.DataMember = "Funcionario";
}
}
' VB.NET
Public Class FormFuncionarios
Private DataSet As New DataSetRelatorio
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Opção 2 - DataSet "bindado" no DataGridView
dataGridView1.AutoGenerateColumns = False
dataGridView1.DataSource = DataSet
dataGridView1.DataMember = "Funcionario"
End Sub
End Class
Com isso, ao invés de termos que criar um novo DataSet para passarmos para o relatório, podemos utilizar esse DataSet que acabamos de criar no nível do formulário. Como ele está “bindado” com o grid, todas as linhas que forem criadas no grid serão passadas para o relatório. Nesse caso, o código para exibirmos o formulário do relatório fica muito mais simples:
// C#
// Opção 2 - DataSet "bindado" no DataGridView
var formRelatorio = new FormRelatorio(_dataSet);
formRelatorio.Show();
' VB.NET
' Opção 2 - DataSet "bindado" no DataGridView
Dim FormRelatorio = New FormRelatorio(DataSet)
FormRelatorio.Show()
Atenção! Pode ser que você receba um erro parecido com a imagem abaixo ao tentar criar uma nova linha no grid:
Se isso acontecer, significa que ficou faltando configurar a propriedade “DataPropertyName” nas colunas no grid. Essa propriedade serve para ligar as colunas do grid com as colunas da DataTable:
Criando o relatório com uma classe
Agora que já vimos como exibimos o relatório do Crystal Reports utilizando um DataSet, vamos conferir como podemos alimentar o mesmo relatório utilizando instâncias de uma classe? Para isso, vamos adicionar uma nova classe no nosso projeto. Essa classe terá a mesma estrutura da DataTable que criamos anteriormente:
// C#
public class DadosRelatorio
{
public string FuncionarioID { get; set; }
public string Nome { get; set; }
public string Sobrenome { get; set; }
}
' VB.NET
Public Class DadosRelatorio
Public Property FuncionarioID As String
Public Property Nome As String
Public Property Sobrenome As String
End Class
Com essa nova classe criada, na hora de exibirmos o relatório nós teremos que iterar pelas linhas do grid montando uma lista de instâncias dessa classe baseada nas informações cadastradas no grid. Porém, antes disso, temos que adicionar um construtor no formulário do relatório. Esse novo construtor será muito parecido com o anterior (que recebe um DataSet), mas, nesse caso, ele deverá receber uma coleção (IEnumerable) que alimentará o relatório:
' VB.NET
Public Sub New(Colecao As IEnumerable)
Me.New()
RelatorioFuncionario1.SetDataSource(Colecao)
CrystalReportViewer1.RefreshReport()
End Sub
Por fim, vamos ajustar o código do botão “Relatório” para criar uma lista de “DadosRelatorio“, passando-a para o “FormRelatorio“:
// C#
// Opção 3 - Criando uma lista "na mão":
var lista = new List<DadosRelatorio>();
foreach (DataGridViewRow linha in dataGridView1.Rows)
{
if (!linha.IsNewRow)
{
var novoItem = new DadosRelatorio();
novoItem.FuncionarioID = linha.Cells["FuncionarioID"].Value.ToString();
if (linha.Cells["Nome"].Value != null)
novoItem.Nome = linha.Cells["Nome"].Value.ToString();
if (linha.Cells["Sobrenome"].Value != null)
novoItem.Sobrenome = linha.Cells["Sobrenome"].Value.ToString();
lista.Add(novoItem);
}
}
var formRelatorio = new FormRelatorio(lista);
formRelatorio.Show();
' VB.NET
' Opção 3 - Criando uma lista "na mão":
Dim Lista As New List(Of DadosRelatorio)
For Each Linha As DataGridViewRow In dataGridView1.Rows
If Not Linha.IsNewRow Then
Dim NovoItem = New DadosRelatorio()
NovoItem.FuncionarioID = Linha.Cells("FuncionarioID").Value.ToString()
If Linha.Cells("Nome").Value <> Nothing Then
NovoItem.Nome = Linha.Cells("Nome").Value.ToString()
End If
If Linha.Cells("Sobrenome").Value <> Nothing Then
NovoItem.Sobrenome = Linha.Cells("Sobrenome").Value.ToString()
End If
Lista.Add(NovoItem)
End If
Next
Dim FormRelatorio As New FormRelatorio(Lista)
FormRelatorio.Show()
Da mesma forma que podemos “bindar” um DataSet com o grid, nós podemos também “bindar” uma lista com o grid. Dessa forma, nós conseguimos simplificar o código da exibição do relatório, uma vez que não seria mais necessária a criação manual da lista na hora de exibirmos o relatório. Para isso, temos que criar uma “BindingList” de “DadosRelatorio” no nível do formulário e setarmos essa lista como “DataSource” do grid no construtor do formulário:
// C#
public partial class FormFuncionarios : Form
{
BindingList<DadosRelatorio> _lista = new BindingList<DadosRelatorio>();
public FormFuncionarios()
{
InitializeComponent();
// Opção 4 - Lista "bindada" no DataGridView
dataGridView1.AutoGenerateColumns = false;
dataGridView1.DataSource = _lista;
}
}
' VB.NET
Public Class FormFuncionarios
Private Lista As New System.ComponentModel.BindingList(Of DadosRelatorio)
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Opção 4 - Lista "bindada" no DataGridView
dataGridView1.AutoGenerateColumns = False
dataGridView1.DataSource = Lista
End Sub
End Class
Por fim, o código para exibirmos o relatório ficaria bem simples:
// C#
// Opção 4 - Lista "bindada" no DataGridView
var formRelatorio = new FormRelatorio(_lista);
formRelatorio.Show();
' VB.NET
' Opção 4 - Lista "bindada" no DataGridView
Dim FormRelatorio As New FormRelatorio(Lista)
FormRelatorio.Show()
Concluindo
Apesar de ser possível criarmos a estrutura de dados de um relatório do Crystal Reports sem utilizarmos um DataSet ou classe (utilizando arquivos TTX), essa não é uma prática recomendada. Como já teremos que criar um DataSet ou classe para alimentarmos o nosso relatório, vale mais a pena criar esse DataSet ou classe antes de confeccionarmos o relatório.
Neste artigo você aprendeu a imprimir o conteúdo de um DataGridView no Crystal Reports, alimentando o relatório com um DataSet ou uma coleção de instâncias de uma classe. Você viu também que é mais recomendado “bindar” o DataSet ou a coleção diretamente com o grid ao invés de ter que construir os dados do relatório antes da sua exibição.
E você, já teve que fazer algo parecido? Como é que você acabou resolvendo essa situação? Conte mais detalhes para a gente na caixa de comentários.
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Ao responder questões que recebo por e-mail ou quando tiro um tempinho para dar uma olhada nos fóruns da MSDN, tenho percebido ultimamente que muitos programadores .NET ainda não conhecem arquivos de recursos! Essa é uma funcionalidade tão útil quando estamos desenvolvendo aplicativos com o C# ou VB.NET e eu acho uma pena que tanta gente ainda não conheça ou não saiba utilizar. Pensando nessa deficiência, resolvi escrever este artigo detalhando como utilizar arquivos de recursos no C# e VB.NET.
O que são arquivos de recursos?
Primeiramente, antes de mostrar como podemos utiliza-los, vamos entender o que são os arquivos de recursos. Eles nada mais são do que arquivos que apontam para recursos (como strings, imagens, ícones, arquivos, etc.) que poderão ser utilizados na nossa aplicação. Esses arquivos têm a extensão “resx” e, na verdade, são um atalho para os arquivos físicos que ficam normalmente armazenados na pasta “Resources” do nosso projeto.
Onde ficam os arquivos de recursos?
Quando criamos um projeto desktop (Windows Forms ou WPF) com o C# ou VB.NET, o próprio Visual Studio já cria automaticamente um arquivo de recursos que podemos utilizar no projeto todo. No C#, esse arquivo fica armazenado dentro da seção “Properties” do “Solution Explorer“:
Já no VB.NET, o arquivo de recursos que é criado automaticamente pelo Visual Studio pode ser acessado na tela de propriedades do projeto:
Tipos de recursos suportados
Clicando no botão para escolha do tipo de recurso, percebemos que podemos adicionar praticamente qualquer coisa dentro do arquivo de recursos:
As opções vão desde strings, imagens e ícones até arquivos de mídia (sons e vídeos) ou qualquer outro tipo de arquivo (como PDFs, DOCs, etc). Tudo pode ser armazenado dentro do arquivo de recursos, caso necessário.
Adicionando novos recursos
Para adicionarmos novos recursos, basta clicarmos no botão “Add Resource” e escolher o tipo de recurso que queremos adicionar:
Na maior parte do tempo, vamos trabalhar principalmente com duas opções: nova string ou arquivo existente. Ao adicionarmos um arquivo existente, ele automaticamente será adicionado na categoria pertinente (quando adicionamos um arquivo “.bmp” pré-existente, ele será automaticamente adicionado na categoria “imagens“).
Além dessas opções, o Visual Studio também conta com um simples editor de imagens, ícones e arquivos de texto para criarmos novos recursos diretamente pelo Visual Studio (ao invés de selecionarmos um arquivo pré-existente).
Agora, imagine que temos uma string longa que será utilizada várias vezes no nosso sistema em diferentes lugares. Ao invés de digitarmos esse texto várias vezes diretamente nos controles, podemos adicionar essa string como um recurso que posteriormente poderá ser utilizado quantas vezes forem necessárias. Para adicionarmos uma nova string no arquivo de recursos, basta irmos até a seção de strings do arquivo de recursos e adicionarmos uma nova entrada na lista:
O código para acessarmos esse recurso através do código é um pouco diferente quando comparamos o C# com o VB.NET. No C#, acessamos o conteúdo do arquivo de recursos através do seu namespace (no caso do arquivo de recursos padrão, ele fica dentro de “Properties.Resources“). Já no VB.NET, acessamos os recursos através da propriedade “My.Resources“.
Para conferirmos essa utilização, vamos adicionar uma Label no nosso formulário e, no evento “Load” do formulário, vamos configurar o seu texto para a string que acabamos de criar:
' VB.NET
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Label1.Text = My.Resources.MinhaString
End Sub
Execute a aplicação e veja o resultado:
Trabalhando com imagens no arquivo de recursos
Da mesma forma que podemos adicionar strings nos arquivos de recursos, o processo para adicionarmos imagens também é muito fácil. Primeiramente, vamos escolher uma imagem para testarmos. Eu escolhi essa aqui no Pixabay (estou na “vibe” de bebês porque meu segundo filho nasceu há dois meses). Se quiser baixar diretamente aqui do meu site, clique aqui.
Uma vez baixado o arquivo para o seu computador, escolha a opção para adicionar um novo recurso através de um arquivo pré-existente e selecione a imagem que acabamos de baixar. Note que, após a inclusão do recurso, podemos alterar o nome do recurso:
Para facilitar a nossa vida, vamos renomear esse recurso, dando o nome de “Baby” para ele. Feito isso, vamos adicionar um PictureBox no nosso formulário e vamos clicar na opção “Choose Image“:
Agora, note que, dentro da tela “Choose Resource“, nós conseguimos encontrar a imagem que acabamos de adicionar no arquivo de recursos:
Caso quiséssemos, nós poderíamos acessar essa imagem via código também:
E outros tipos de arquivos, como PDF?
Como mencionei anteriormente, podemos adicionar qualquer tipo de arquivo dentro dos nossos arquivos de recursos. Quando o arquivo que está sendo adicionado não se enquadra em nenhuma das categorias pré-estabelecidas, ele simplesmente será adicionado como um “arquivo“. Por exemplo, se quisermos adicionar esse PDF no arquivo de recursos, poderemos acessá-lo como um array de bytes via código:
Nós não precisamos ficar limitados com o arquivo de recursos “padrão” que é criado automaticamente pelo Visual Studio. Através da opção “Add New Item” do nosso projeto, podemos adicionar outros arquivos de recursos (ou até mesmo em um outro projeto, como em uma “Class Library“):
Em seguida, para utilizarmos os recursos armazenados dentro desse outro arquivo, basta utilizarmos o nome completo do recurso (no C#) ou a propriedade “My.Resources” seguida do nome do arquivo de recursos (no VB.NET):
Atenção! Os recursos aumentam o tamanho do executável!
Uma coisa que devemos ficar atentos é que todos os arquivos adicionados nos arquivos de recursos do nosso projeto serão compilados juntamente com a aplicação (ou com a biblioteca, se os arquivos de recursos estiverem armazenados dentro de uma dll). Dessa forma, o tamanho do executável ou dll aumentará significativamente dependendo do tamanho dos itens que adicionarmos nos arquivos de recursos. Veja só o tamanho dessa simples aplicação de exemplo que criamos neste artigo:
E como funciona no WPF? E no MVC?
Tudo o que vimos neste artigo sobre arquivos de recursos não vale somente para o Windows Forms. O WPF se comporta exatamente da mesma maneira. E existem artigos mostrando como utilizar arquivos de recursos no Web Forms e no MVC.
Concluindo
Os arquivos de recursos, apesar de serem uma funcionalidade básica de aplicações desenvolvidas com o .NET Framework, é muitas vezes desconhecido pelos programadores desse tipo de aplicação. Eles ajudam muito na organização dos nossos projetos e é uma funcionalidade que todo programador deveria pelo menos conhecer.
Neste artigo você conferiu como utilizar o arquivo de recursos “padrão” que é criado automaticamente pelo Visual Studio nos projetos Windows Forms e WPF. Além disso, você conferiu também como adicionar outros arquivos de recursos no C# e VB.NET, além do que é criado automaticamente pelo Visual Studio. Por fim, você viu que devemos ficar atentos com o tamanho da aplicação, que terá um crescimento diretamente relacionado com o tamanho dos itens que adicionarmos nos arquivos de recursos.
E você? Utiliza arquivos de recursos nos seus projetos? Qual a sua opinião sobre eles? Aprova ou não aprova? Deixe as suas observações na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
A cada dia que passa, aumenta a necessidade de criarmos diferentes versões dos nossos aplicativos, cada uma voltada para uma plataforma diferente. No mundo cada vez mais conectado de hoje em dia, não é raro termos o requisito que a nossa aplicação funcione em dispositivos móveis, além do desktop (ou web). Mas, e se a nossa aplicação utiliza o Report Viewer para gerar relatórios? Como fazer com que o mecanismo de geração de relatórios seja implementado somente em um lugar? Uma das opções é expormos PDFs gerados pelo Report Viewer com Web API. E é justamente isso que você vai conferir no artigo de hoje.
Criando o projeto de Web API
A ideia de criarmos uma Web API para gerarmos PDFs do relatório faz com que toda a lógica de geração dos relatórios seja separada neste projeto de Web API. Dessa forma, não temos repetição de código, além de tornar os relatórios compatíveis com todas as plataformas, uma vez que conseguimos exibir PDFs em praticamente qualquer lugar.
Vamos começar criando um novo projeto do tipo “ASP.NET Web Application” e escolhendo o template da “Web API“. Para facilitar o processo, desabilitei a autenticação e a hospedagem no Azure:
Criando o relatório
Com o projeto criado, vamos adicionar um novo DataSet dentro da pasta “Models“, que servirá de fonte de dados para o relatório. Vamos dar o nome de “DataSetFuncionario” para esse novo DataSet e, dentro dele, vamos criar uma nova DataTable chamada “Funcionario“:
Nota: não é obrigatório o uso de DataSets com o Report Viewer. Se você já tiver o arquivo .rdlc gerado utilizando um outro tipo de fonte de dados, você pode utilizá-lo sem problema na sua Web API. Por exemplo, é possível criarmos os relatórios .rdlc utilizando as classes de domínio do nosso projeto, como mostrei neste artigo sobre o Report Viewer com o Entity Framework.
Agora que já temos o DataSet criado, vamos adicionar uma nova pasta ao nosso projeto. Daremos o nome de “Reports” para essa pasta, e é dentro dela que armazenaremos os nossos relatórios:
Uma vez criada a pasta “Reports“, adicione um novo item do tipo “Report” dentro dessa pasta, dando o nome de “ReportFuncionario“:
Feito isso, adicione um novo DataSet no relatório, escolhendo o DataSet que criamos anteriormente e a DataTable “Funcionario“:
Em seguida, adicione uma tabela e arraste os campos do DataSet para dentro das colunas da tabela, de forma que o layout fique parecido com a imagem abaixo:
Expondo o PDF do relatório
Com o relatório criado, vamos partir para a criação do controller que “servirá” o PDF do relatório. Porém, antes de criarmos um novo controller, precisamos adicionar no nosso projeto a referência à dll do Report Viewer, que fica dentro da categoria “Extensions” da tela de adição de referências:
Agora que já temos a referência para a dll do Report Viewer, vamos criar um novo controller dentro da pasta “Controllers“. Escolha o template “Web API 2 Controller – Empty” (uma vez que nós só criaremos manualmente o método “Get” dentro desse controller) e escolha o nome de “RelatorioController“:
O método para gerarmos o PDF do relatório é muito simples. Primeiramente, precisamos criar o DataSet e uma instância de “LocalReport” passando o DataSet criado. Em seguida, utilizamos o método “Render” para gerarmos o PDF, resultando em um array de bytes. Por fim, retornamos o array de bytes como conteúdo de uma HttpResponseMessage:
// C#
public class RelatorioController : ApiController
{
public HttpResponseMessage Get()
{
var dataSet = new Models.DataSetFuncionario();
dataSet.Funcionario.AddFuncionarioRow("André", "Lima", DateTime.Now);
dataSet.Funcionario.AddFuncionarioRow("Fulano", "de Tal", DateTime.Now);
dataSet.Funcionario.AddFuncionarioRow("Beltrano", "da Silva", DateTime.Now);
var report = new Microsoft.Reporting.WebForms.LocalReport();
report.ReportPath = System.Web.Hosting.HostingEnvironment.MapPath("~/Reports/ReportFuncionario.rdlc");
report.DataSources.Add(new Microsoft.Reporting.WebForms.ReportDataSource("DataSetFuncionario", (System.Data.DataTable)dataSet.Funcionario));
report.Refresh();
string mimeType = "";
string encoding = "";
string filenameExtension = "";
string[] streams = null;
Microsoft.Reporting.WebForms.Warning[] warnings = null;
byte[] bytes = report.Render("PDF", null, out mimeType, out encoding, out filenameExtension, out streams, out warnings);
HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
result.Content = new ByteArrayContent(bytes);
result.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mimeType);
return result;
}
}
' VB.NET
Public Class RelatorioController
Inherits ApiController
Public Function GetValues() As Http.HttpResponseMessage
Dim DataSet As New DataSetFuncionario()
DataSet.Funcionario.AddFuncionarioRow("André", "Lima", DateTime.Now)
DataSet.Funcionario.AddFuncionarioRow("Fulano", "de Tal", DateTime.Now)
DataSet.Funcionario.AddFuncionarioRow("Beltrano", "da Silva", DateTime.Now)
Dim Report As New Microsoft.Reporting.WebForms.LocalReport()
Report.ReportPath = System.Web.Hosting.HostingEnvironment.MapPath("~/Reports/ReportFuncionario.rdlc")
Report.DataSources.Add(New Microsoft.Reporting.WebForms.ReportDataSource("DataSetFuncionario", DirectCast(DataSet.Funcionario, System.Data.DataTable)))
Report.Refresh()
Dim MimeType As String = ""
Dim Encoding As String = ""
Dim FilenameExtension As String = ""
Dim Streams As String() = Nothing
Dim Warnings As Microsoft.Reporting.WebForms.Warning() = Nothing
Dim Bytes As Byte() = Report.Render("PDF", Nothing, MimeType, Encoding, FilenameExtension, Streams, Warnings)
Dim Result As Http.HttpResponseMessage = New Http.HttpResponseMessage(HttpStatusCode.OK)
Result.Content = New Http.ByteArrayContent(Bytes)
Result.Content.Headers.ContentType = New System.Net.Http.Headers.MediaTypeHeaderValue(MimeType)
Return Result
End Function
End Class
Execute a aplicação e, na janela do browser, adicione o final “/api/Relatorio” na URL da API para ver o PDF sendo gerado:
Possível melhoria
No exemplo que vimos até agora, criamos um controller para exibirmos um relatório. Obviamente, em um projeto com vários relatórios, isso se transformaria rapidamente em um pesadelo. Uma possível melhoria que poderíamos implementar nesse caso é termos somente um controller responsável pela geração dos PDFs e esse controller receberia o nome do relatório a ser gerado. Isso poderia ser feito adicionando um parâmetro string no método “Get” da API. Dessa forma, poderíamos hipoteticamente obter, por exemplo, o relatório de funcionários através da URL “/api/Relatorios/Funcionarios” e o relatório de clientes através da URL “/api/Relatorios/Clientes“:
// C#
public HttpResponseMessage Get(string id)
{
switch (id.ToUpper())
{
case "FUNCIONARIOS":
return GetRelatorioFuncionarios();
case "CLIENTES":
return GetRelatorioClientes();
default:
return null;
}
}
' VB.NET
Public Function GetValue(ByVal id As String) As Http.HttpResponseMessage
Select Case id.ToUpper()
Case "FUNCIONARIOS"
Return GetRelatorioFuncionarios()
Case "CLIENTES"
Return GetRelatorioClientes()
Case Else
Return Nothing
End Select
End Function
Acessando a Web API em outros projetos
Uma vez tendo a API em execução, podemos executá-la de qualquer tipo de aplicativo que suporte requisições HTTP. Por exemplo, em uma Console Application, poderíamos recuperar o PDF e exibi-lo na ferramenta padrão através deste código:
// C#
var request = System.Net.WebRequest.Create("http://localhost:34321/api/Relatorio");
var response = request.GetResponse();
using (var fileStream = System.IO.File.Create("arquivo.pdf"))
{
response.GetResponseStream().CopyTo(fileStream);
}
System.Diagnostics.Process.Start("arquivo.pdf");
' VB.NET
Dim Request = System.Net.WebRequest.Create("http://localhost:34321/api/Relatorio")
Dim Response = Request.GetResponse()
Using fileStream = System.IO.File.Create("arquivo.pdf")
Response.GetResponseStream().CopyTo(fileStream)
End Using
System.Diagnostics.Process.Start("arquivo.pdf")
Concluindo
A criação de Web APIs para expormos PDFs dos nossos relatórios do Report Viewer é um artifício que podemos utilizar para termos acesso aos nossos relatórios através de aplicações que não suportem o controle do Report Viewer (como aplicações mobile). Neste artigo você aprendeu a montar rapidamente uma Web API expondo o PDF de um relatório do Report Viewer. Vimos também como podemos acessar essa Web API através de uma Console Application, onde abrimos o PDF gerado pela API.
E você, já teve a necessidade de gerar um relatório do Report Viewer numa plataforma não suportada? Como você acabou resolvendo essa necessidade? Através de uma Web API, algum outro tipo de serviço ou alguma outra alternativa? Conte-nos mais detalhes na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
O que seria um relatório sem regras que alterem a sua formatação dependendo de condições de negócios? Muito provavelmente seria um relatório bem “chato“, somente listando alguns valores e deixando 100% da sua interpretação a cargo do usuário. Se você está acostumado a entregar os seus relatórios dessa maneira “sem graça“, já passou da hora de mudar essa realidade. Uns tempos atrás eu mostrei como trabalhar com expressões no Report Viewer e hoje é a vez do Crystal Reports. No artigo de hoje, confira como adicionar formatação condicional no Crystal Reports.
Criando o projeto e a classe de domínio
Para entendermos a formatação condicional no Crystal Reports, temos que primeiramente criar um relatório. Para simplificar o entendimento, o nosso relatório será uma simples listagem, porém, com algumas formatações dependendo de regras de negócios.
Primeiramente, vamos começar criando um novo projeto do tipo “Windows Forms Application“. No final das contas, não importa a plataforma que você escolha, uma vez que trabalharemos a formatação no próprio relatório (arquivo .rpt), que seria igual para qualquer plataforma que você estiver trabalhando. Eu só escolhi Windows Forms por ser mais fácil e por ser a plataforma mais utilizada.
Dentro do projeto, vamos criar uma pequena classe de domínio chamada “Produto“. Essa classe terá algumas propriedades, que serão todas posteriormente listadas no relatório:
// C#
public class Produto
{
public int ProdutoId { get; set; }
public string Nome { get; set; }
public decimal Preco { get; set; }
public int EstoqueMinimo { get; set; }
public int EstoqueAtual { get; set; }
public bool Descontinuado { get; set; }
}
' VB.NET
Public Class Produto
Public Property ProdutoId As Integer
Public Property Nome As String
Public Property Preco As Decimal
Public Property EstoqueMinimo As Integer
Public Property EstoqueAtual As Integer
Public Property Descontinuado As Boolean
End Class
Criando o esqueleto do relatório
Com a classe de domínio criada, vamos partir para a criação do esqueleto do relatório. Para isso, adicione no projeto um novo item do tipo “Crystal Reports“. Esse tipo de item fica dentro da categoria “Reporting“. Dê o nome de “RelatorioProdutos” para este novo item:
A partir daqui, temos duas opções: ou criamos o relatório em branco ou utilizamos o assistente. Neste artigo eu vou prosseguir com o relatório em branco, mas, se você quiser seguir com o assistente, não fará diferença alguma.
Uma vez criado o relatório, temos que configurar a fonte de dados do relatório. Para isso, vamos até a janela de “Field Explorer“, clicamos com o botão direito em “Database Fields” e escolhemos a opção “Database Expert“:
Em seguida, temos que encontrar a nossa classe “Produto” para jogarmos para o lado “Selected Tables“. Encontramos essa classe dentro da categoria “.NET Objects“:
Por fim, vamos arrastar os campos da janela “Field Explorer” para dentro da área de detalhes do relatório e vamos adicionar um título ao relatório, de forma que ele fique parecido com a imagem abaixo:
Exibindo o relatório
Agora que já temos o nosso relatório em mãos, vamos exibir esse relatório no nosso formulário. Para isso, temos que arrastar um controle do Crystal Reports para dentro do formulário e, em seguida, temos que escolher o relatório que acabamos de criar:
Feito isso, vamos adicionar um pouquinho de código no code-behind do formulário para criarmos e passarmos uma lista de Produtos para o relatório. Faremos isso com um laço “for” e alguns dados aleatórios. Obviamente, no seu projeto “de verdade“, você teria que recuperar os dados do banco:
// C#
public Form1()
{
InitializeComponent();
var rand = new Random();
var lista = new List<Produto>();
for (int contador = 1; contador <= 100; contador++)
{
lista.Add(new Produto()
{
ProdutoId = contador,
Nome = "Produto " + contador,
Preco = Convert.ToDecimal(rand.NextDouble() * 100),
EstoqueMinimo = rand.Next(1, 1000),
EstoqueAtual = rand.Next(1, 1000),
Descontinuado = Convert.ToBoolean(rand.Next(-1, 1))
});
}
RelatorioProdutos1.SetDataSource(lista);
RelatorioProdutos1.Refresh();
}
' VB.NET
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim Rand As Random = New Random()
Dim Lista As List(Of Produto) = New List(Of Produto)
For Contador As Integer = 1 To 100
Lista.Add(New Produto() With
{
.ProdutoId = Contador,
.Nome = "Produto " & Contador,
.Preco = Convert.ToDecimal(Rand.NextDouble() * 100),
.EstoqueMinimo = Rand.Next(1, 1000),
.EstoqueAtual = Rand.Next(1, 1000),
.Descontinuado = Convert.ToBoolean(Rand.Next(-1, 1))
})
Next
RelatorioProdutos1.SetDataSource(Lista)
RelatorioProdutos1.Refresh()
End Sub
Execute o projeto e veja o resultado do relatório sem a formatação condicional:
Nota: se você receber uma “FileNotFoundException” vindo do controle do Crystal Reports ao executar o projeto, isso quer dizer que falta uma configuração no seu arquivo app.config. Eu mostrei como fazer essa configuração no artigo: Trabalhando com o Crystal Reports no WPF.
Adicionando formatação condicional
E agora, André? Como é que eu adiciono formatação condicional nesse relatório? Não se preocupe. É justamente isso que eu vou mostrar para você a partir daqui. Que tal alterarmos a cor da fonte de “Estoque mínimo” para vermelho caso ele seja menor que o “Estoque atual“?
O Crystal Reports é extremamente flexível no que diz respeito à formatação condicional. Diferente do Report Viewer, ele suporta a criação de fórmulas para praticamente qualquer propriedade do relatório. Todo lugar onde você encontrar um botão com um símbolo “x2” quer dizer que você pode adicionar uma fórmula para aquela propriedade.
Por exemplo, para adicionarmos a formatação do “Estoque mínimo“, temos que abrir a formatação do controle clicando com o botão direito e escolhendo a opção “Format Object“:
Em seguida, temos que ir até a aba “Font“. Veja só os botões “x2” que eu mencionei anteriormente:
No nosso caso, como queremos adicionar uma lógica referente à cor da célula, temos que clicar no botão “x2” da propriedade “Color“. Quando clicamos no botão, a janela de edição de fórmula do Crystal Reports será aberta. Dentro dessa janela, temos que colocar a expressão que deverá retornar “vermelho” caso o “Estoque atual” seja menor que o “Estoque mínimo” ou preto caso contrário.
As fórmulas do Crystal Reports podem ser escritas de duas maneiras: utilizando a sintaxe do próprio Crystal Reports (que é tipo um Pascal misturado com Basic e C) ou utilizando a sintaxe Basic. Eu nunca vi ninguém utilizar a sintaxe Basic em relatórios do Crystal Reports, mas, é totalmente possível. Essa opção pode ser bem útil caso você esteja acostumado a desenvolver relatórios no Report Viewer ou caso você trabalhe com VB.NET.
Para implementarmos essa lógica da cor, utilizamos a seguinte fórmula:
if ({CrystalFormatacaoCustomizada_Produto.EstoqueAtual} < {CrystalFormatacaoCustomizada_Produto.EstoqueMinimo}) then
crRed
else
crBlack
Ou, caso você prefira trabalhar com a sintaxe em Basic, a fórmula seria esta:
formula = IIf({CrystalFormatacaoCustomizada_Produto.EstoqueAtual} < {CrystalFormatacaoCustomizada_Produto.EstoqueMinimo}, crRed, crBlack)
Para escolher a sintaxe da fórmula, utilize esse dropdown:
Dentro do mesmo relatório você pode ter algumas fórmulas com a sintaxe do Crystal Reports e outras fórmulas com sintaxe Basic.
Escondendo linhas baseando-se em uma condição
Agora que já vimos como trabalhar a formatação de textos baseando-se em uma fórmula, vamos ver como podemos esconder registros utilizando uma regra de negócio. No nosso exemplo, vamos supor que queiramos esconder os produtos descontinuados.
É possível adicionarmos lógicas de negócio para cada seção do relatório (cabeçalho do relatório, cabeçalho de página, detalhes, rodapés, etc). Para isso, basta clicarmos na seção desejada (no nosso caso, a seção de detalhes) e então, temos que clicar na opção “Section Expert“:
Note que, dentro da janela “Section Expert“, temos os famosos botões “x2” para as propriedades da seção:
No nosso caso, vamos adicionar uma fórmula na propriedade “Suppress“. A fórmula deverá retornar “true” caso o registro deva ser escondido, e “false” caso contrário. Dessa forma, para escondermos os produtos descontinuados, nós só temos que utilizar o campo “Descontinuado” da nossa tabela de produtos:
Como você pode notar, nos produtos em que “Estoque atual” é menor que “Estoque mínimo“, a célula correspondente ao “Estoque atual” está pintada de vermelho. Além disso, note que alguns produtos foram escondidos (Ids 3, 5, 6, etc). Esses produtos foram escondidos porque estão descontinuados.
Concluindo
O Crystal Reports é extremamente flexível no que diz respeito à adição de lógicas de negócio para as propriedades dos seus controles e seções. Nós podemos adicionar fórmulas em praticamente todas as propriedades de todos os controles. Essas fórmulas podem ser escritas tanto utilizando a sintaxe do próprio Crystal Reports quanto a sintaxe em Basic.
Neste artigo você conferiu como trabalhar com formatação condicional no Crystal Reports, adicionando uma fórmula na propriedade “Color” de uma célula, de forma que ela seja pintada de vermelho em algumas situações. Além disso, você aprendeu também a esconder registros do relatório baseando-se em uma fórmula.
E você, já utilizou as fórmulas do Crystal Reports? Qual foi a fórmula mais maluca que você já teve que implementar nos seus relatórios? Você costuma utilizar a sintaxe do Crystal Reports ou a sintaxe em Basic? Estamos todos curiosos para saber, então, conte-nos mais detalhes na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Hoje em dia é impossível pensar em uma aplicação comercial que não tenha algum campo para a entrada de endereços em algum lugar do aplicativo. Dito isso, uma funcionalidade que podemos implementar muito facilmente e que facilitaria a vida do usuário é o preenchimento do endereço baseado no CEP. Antigamente isso só era possível através da compra de um banco de dados de CEPs. Atualmente, com os web services dos Correios nós podemos facilmente fazer essa consulta gratuitamente (pelo menos por enquanto). No artigo de hoje você vai conferir como acessar os web services dos Correios com C# e VB.NET para consultarmos CEPs e calcularmos preços de encomendas SEDEX ou PAC.
Quais são os web services disponibilizados pelos Correios?
No momento da escrita deste artigo (julho de 2016), os Correios disponibilizam quatro web services:
Sistema de Gerenciamento de Postagens dos Correios (SIGEP): esse primeiro web service possui métodos relacionados ao envio de encomendas, como geração de códigos para etiquetas, busca de clientes e consulta de CEPs. A maioria dos métodos necessita que você tenha um contrato com os Correios, porém, o método que vamos utilizar neste artigo (consulta de CEPs) não necessita de autenticação.
Logística reversa: sinceramente não entendi para que serve esse web service. O pessoal que entende de logística ou que trabalha com sistemas desse tipo deve entender. Se quiser deixar um comentário explicando esse conceito, fique à vontade.
Rastreamento de objetos: como o próprio nome já diz, ele possibilita o rastreio de objetos com base no código de 13 dígitos gerado pelos Correios. Não testei esse web service, mas, pelo que pude observar na documentação, ele precisa de um usuário e senha.
Calculador de preços e prazos de encomendas: faz o cálculo do preço e prazo de entrega para encomendas SEDEX e PAC. Se você não tiver um contrato com os Correios, ele também retornará os valores, mas, o preço será baseado na tabela geral (o mesmo preço gerado para qualquer pessoa através do site dos Correios).
Inacreditavelmente, existem documentos dos próprios Correios explicando a estrutura desses web services. Eles não são lá muito detalhados, mas, já é um bom começo. Os web services do SIGEP, logística reversa e rastreamento de objetos estão documentados aqui. Já o web service de cálculo de preços e encomendas está documentado aqui.
Consultando um CEP
O primeiro web service que vamos utilizar é o web service do SIGEP. Esse web service possui diversos métodos, mas, o que mais nos interessa para este artigo é o método que retorna o endereço correspondente a um CEP.
Para implementarmos essa consulta, vamos criar um novo projeto do tipo “Console Application“. Nós poderíamos criar qualquer tipo de projeto (como Windows Forms, WPF, Web Forms ou MVC), mas, para facilitar a nossa vida e focarmos diretamente nos web services, acredito que uma “Console Application” seja o tipo ideal de projeto.
Uma vez criado o projeto, vamos adicionar a referência para o primeiro web service dos Correios. Para isso, vá até o “Solution Explorer“, clique com o botão direito sobre “References” e escolha a opção “Add Service Reference“:
Na janela que se abre, temos que entrar com a URL da definição do web service que queremos acessar. No caso do web service do SIGEP, o endereço é o seguinte:
Digite esse endereço na caixa “Address“, clique em “Go“, escolha o serviço “AtendeCliente“, dê o nome de “WSCorreios” para a referência e clique em “OK”:
Pronto! Com isso já podemos acessar o web service para consultarmos o endereço relacionado a um CEP. O código é extremamente simples. Primeiramente temos que criar uma instância do “client” do web service e depois chamamos o método “consultaCEP” passando o CEP desejado. O resultado será retornado em uma variável do tipo “enderecoERP“, que possui todas as propriedades do endereço (como bairro, cidade, UF, etc):
// C#
System.Console.Write("Digite o CEP: ");
var valor = System.Console.ReadLine();
try
{
var ws = new WSCorreios.AtendeClienteClient();
var resposta = ws.consultaCEP(valor);
System.Console.WriteLine();
System.Console.WriteLine("Endereço: {0}", resposta.end);
System.Console.WriteLine("Complemento: {0}", resposta.complemento);
System.Console.WriteLine("Complemento 2: {0}", resposta.complemento2);
System.Console.WriteLine("Bairro: {0}", resposta.bairro);
System.Console.WriteLine("Cidade: {0}", resposta.cidade);
System.Console.WriteLine("Estado: {0}", resposta.uf);
System.Console.WriteLine("Unidades de Postagem: {0}", resposta.unidadesPostagem);
}
catch (Exception ex)
{
System.Console.WriteLine("Erro ao efetuar busca do CEP: {0}", ex.Message);
}
System.Console.ReadLine();
' VB.NET
System.Console.Write("Digite o CEP: ")
Dim Valor = System.Console.ReadLine()
Try
Dim WS = New WSCorreios.AtendeClienteClient()
Dim Resposta = WS.consultaCEP(Valor)
System.Console.WriteLine()
System.Console.WriteLine("Endereço: {0}", Resposta.end)
System.Console.WriteLine("Complemento: {0}", Resposta.complemento)
System.Console.WriteLine("Complemento 2: {0}", Resposta.complemento2)
System.Console.WriteLine("Bairro: {0}", Resposta.bairro)
System.Console.WriteLine("Cidade: {0}", Resposta.cidade)
System.Console.WriteLine("Estado: {0}", Resposta.uf)
System.Console.WriteLine("Unidades de Postagem: {0}", Resposta.unidadesPostagem)
Catch Ex As Exception
System.Console.WriteLine("Erro ao efetuar busca do CEP: {0}", Ex.Message)
End Try
System.Console.ReadLine()
O CEP pode ser consultado tanto utilizando o hífen (exemplo 13484-339) ou sem hífen (13484339). Veja os resultados das consultas, inclusive contendo alguns cenários em que passamos CEPs inválidos para o serviço:
Calculando o preço e prazo de uma encomenda SEDEX ou PAC
Agora que já vimos a utilização do serviço do SIGEP para consulta de CEPs, vamos ver como podemos utilizar o outro web service dos Correios para fazermos o cálculo de uma encomenda SEDEX ou PAC.
Primeiramente, temos que adicionar outra referência de serviço no nosso projeto, dessa vez utilizando o nome “WSCorreiosPreco” e a seguinte URL:
Esse web service possui diversos métodos relacionados ao cálculo de preços e prazos de encomendas. O método que vamos utilizar é o “CalcPrecoPrazo“, que retorna tanto o preço quanto o prazo da encomenda. Esse método recebe várias informações, como o CEP de origem, CEP de destino, dimensões da encomenda, etc. Entre esses parâmetros, dois merecem uma maior atenção: tipo do envio e formato da encomenda.
O tipo de envio define qual serviço dos Correios que deverá ser utilizado para despachar a encomenda (SEDEX Varejo, SEDEX 10, PAC, etc). As opções para este parâmetro são as seguintes:
Já o formato da encomenda define se a encomenda será despachada em uma caixa, rolo ou envelope. As opções são as seguintes:
Por fim, os parâmetros referentes à entrega em mão própria e aviso de recebimento deverão ser passados no formato S/N (“S” caso o serviço deva ser considerado no cálculo e “N” caso o serviço não deva ser considerado).
Tendo em vista todas essas informações, o código para calcularmos o preço e prazo de uma encomenda ficaria assim:
// C#
System.Console.Write("Digite o código do tipo de envio: ");
var tipoEnvio = System.Console.ReadLine();
System.Console.Write("Digite o CEP de origem: ");
var cepOrigem = System.Console.ReadLine();
System.Console.Write("Digite o CEP de destino: ");
var cepDestino = System.Console.ReadLine();
System.Console.Write("Digite o peso (kg): ");
var peso = System.Console.ReadLine();
System.Console.Write("Digite o código do formato (caixa, envelope, etc): ");
var codigoFormato = System.Console.ReadLine();
System.Console.Write("Digite o comprimento: ");
var comprimento = System.Console.ReadLine();
System.Console.Write("Digite a altura: ");
var altura = System.Console.ReadLine();
System.Console.Write("Digite a largura: ");
var largura = System.Console.ReadLine();
System.Console.Write("Digite o diâmetro: ");
var diametro = System.Console.ReadLine();
System.Console.Write("Entrega em mão própria (S/N)?: ");
var maoPropria = System.Console.ReadLine();
System.Console.Write("Digite o valor declarado: ");
var valorDeclarado = System.Console.ReadLine();
System.Console.Write("Aviso de recebimento (S/N)?: ");
var avisoRecebimento = System.Console.ReadLine();
try
{
int codigoFormatoInt;
if (!int.TryParse(codigoFormato, out codigoFormatoInt))
throw new Exception("Código do formato inválido");
decimal comprimentoDecimal;
if (!decimal.TryParse(comprimento, out comprimentoDecimal))
throw new Exception("Comprimento inválido");
decimal alturaDecimal;
if (!decimal.TryParse(altura, out alturaDecimal))
throw new Exception("Altura inválida");
decimal larguraDecimal;
if (!decimal.TryParse(largura, out larguraDecimal))
throw new Exception("Largura inválida");
decimal diametroDecimal;
if (!decimal.TryParse(diametro, out diametroDecimal))
throw new Exception("Diâmetro inválido");
decimal valorDeclaradoDecimal;
if (!decimal.TryParse(valorDeclarado, out valorDeclaradoDecimal))
throw new Exception("Valor declarado inválido");
var ws = new WSCorreiosPreco.CalcPrecoPrazoWSSoapClient();
var resposta = ws.CalcPrecoPrazo(string.Empty, string.Empty, tipoEnvio, cepOrigem, cepDestino, peso, codigoFormatoInt, comprimentoDecimal, alturaDecimal, larguraDecimal, diametroDecimal, maoPropria, valorDeclaradoDecimal, avisoRecebimento);
var respostaReal = resposta.Servicos.FirstOrDefault();
if (respostaReal != null)
{
System.Console.WriteLine();
System.Console.WriteLine("Prazo: {0} dia", respostaReal.PrazoEntrega);
System.Console.WriteLine("Valor: R$ {0}", respostaReal.Valor);
}
else
{
throw new Exception("Não foi possível encontrar os valores dentro da resposta do serviço");
}
}
catch (Exception ex)
{
System.Console.WriteLine("Erro ao efetuar cálculos: {0}", ex.Message);
}
System.Console.ReadLine();
' VB.NET
System.Console.Write("Digite o código do tipo de envio: ")
Dim TipoEnvio = System.Console.ReadLine()
System.Console.Write("Digite o CEP de origem: ")
Dim CepOrigem = System.Console.ReadLine()
System.Console.Write("Digite o CEP de destino: ")
Dim CepDestino = System.Console.ReadLine()
System.Console.Write("Digite o peso (kg): ")
Dim Peso = System.Console.ReadLine()
System.Console.Write("Digite o código do formato (caixa, envelope, etc): ")
Dim CodigoFormato = System.Console.ReadLine()
System.Console.Write("Digite o comprimento: ")
Dim Comprimento = System.Console.ReadLine()
System.Console.Write("Digite a altura: ")
Dim Altura = System.Console.ReadLine()
System.Console.Write("Digite a largura: ")
Dim Largura = System.Console.ReadLine()
System.Console.Write("Digite o diâmetro: ")
Dim Diametro = System.Console.ReadLine()
System.Console.Write("Entrega em mão própria (S/N)?: ")
Dim MaoPropria = System.Console.ReadLine()
System.Console.Write("Digite o valor declarado: ")
Dim ValorDeclarado = System.Console.ReadLine()
System.Console.Write("Aviso de recebimento (S/N)?: ")
Dim AvisoRecebimento = System.Console.ReadLine()
Try
Dim CodigoFormatoInt As Integer
If (Not Integer.TryParse(CodigoFormato, CodigoFormatoInt)) Then
Throw New Exception("Código do formato inválido")
End If
Dim ComprimentoDecimal As Decimal
If (Not Decimal.TryParse(Comprimento, ComprimentoDecimal)) Then
Throw New Exception("Comprimento inválido")
End If
Dim AlturaDecimal As Decimal
If (Not Decimal.TryParse(Altura, AlturaDecimal)) Then
Throw New Exception("Altura inválida")
End If
Dim LarguraDecimal As Decimal
If (Not Decimal.TryParse(Largura, LarguraDecimal)) Then
Throw New Exception("Largura inválida")
End If
Dim DiametroDecimal As Decimal
If (Not Decimal.TryParse(Diametro, DiametroDecimal)) Then
Throw New Exception("Diâmetro inválido")
End If
Dim ValorDeclaradoDecimal As Decimal
If (Not Decimal.TryParse(ValorDeclarado, ValorDeclaradoDecimal)) Then
Throw New Exception("Valor declarado inválido")
End If
Dim WS As New WSCorreiosPreco.CalcPrecoPrazoWSSoapClient()
Dim Resposta = WS.CalcPrecoPrazo(String.Empty, String.Empty, TipoEnvio, CepOrigem, CepDestino, Peso, CodigoFormatoInt, ComprimentoDecimal, AlturaDecimal, LarguraDecimal, DiametroDecimal, MaoPropria, ValorDeclaradoDecimal, AvisoRecebimento)
Dim RespostaReal = Resposta.Servicos.FirstOrDefault()
If (RespostaReal IsNot Nothing) Then
System.Console.WriteLine()
System.Console.WriteLine("Prazo: {0} dia", RespostaReal.PrazoEntrega)
System.Console.WriteLine("Valor: R$ {0}", RespostaReal.Valor)
Else
Throw New Exception("Não foi possível encontrar os valores dentro da resposta do serviço")
End If
Catch Ex As Exception
System.Console.WriteLine("Erro ao efetuar cálculos: {0}", Ex.Message)
End Try
System.Console.ReadLine()
Entretanto, se tentarmos executar o projeto nesse momento, receberemos o seguinte erro:
Esse erro acontece porque o serviço de cálculo de preços dos Correios adiciona dois EndPoints no arquivo app.config: um utilizando SOAP e outro utilizando SOAP 1.2. Para consertar esse erro, temos que comentar um dos EndPoints no arquivo app.config. Eu, por exemplo, comentei o EndPoint referente ao SOAP 1.2:
Outra maneira de resolvermos esse problema sem alterarmos o app.config seria passarmos o nome do EndPoint a ser utilizado no construtor do serviço.
Após fazermos essa alteração e executarmos a aplicação, conseguiremos calcular os preços e prazos de entrega das encomendas:
Concluindo
Apesar de não encontrarmos muitos artigos detalhados sobre a utilização dos web services dos Correios com C# e VB.NET, você conferiu neste artigo que essa tarefa não é lá tão difícil de ser implementada. Basta sabermos os endereços dos web services, quais métodos utilizar e quais parâmetros passar.
Você já precisou utilizar os web services dos Correios na sua aplicação? Como é que ficou o seu código? Parecido com o código apresentado neste artigo ou você acabou fazendo de outra maneira? Conte-nos mais detalhes na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
O conceito de quebra de páginas no desenvolvimento de relatórios é algo básico a ser dominado. É rara a situação em que um relatório ocupa somente uma página, principalmente quando falamos de relatórios mais complexos. O que pode ser algo muito básico pode se tornar um pesadelo para os desenvolvedores que utilizam o Report Viewer como ferramenta de relatórios. Isso porque não é tão simples e muito menos intuitivo criarmos uma quebra de página condicional no Report Viewer. Mas, não se preocupe. No artigo de hoje eu vou desmistificar esse tema para você.
Relatório sem quebra de páginas
Quando criamos um relatório no Report Viewer, por padrão, ele não tem quebra de página customizada. Ou seja, as informações serão impressas na primeira página e, somente quando não tiver mais espaço na primeira página é que o Report Viewer quebrará o relatório em uma segunda página, e assim por diante.
Para conseguirmos entender o conceito de quebra de páginas, vamos criar um relatório bem simples utilizando um projeto do tipo “Windows Forms Application“. Como eu costumo dizer, poderíamos utilizar outros tipos de projeto (WPF, Web Forms, MVC), mas, escolhi o Windows Forms por ser mais simples.
Vamos supor que nós temos um sistema que gerencia livros. O nosso relatório mostrará uma lista dos livros cadastrados no sistema. Dessa forma, a primeira coisa que temos que fazer é criarmos uma nova classe, chamada “Livro“. Mais para frente, o nosso relatório receberá uma lista de instâncias dessa classe, que será justamente a fonte de dados do relatório:
// C#
public class Livro
{
public int ID { get; set; }
public string Nome { get; set; }
public string GeneroLiterario { get; set; }
public string TipoGeneroLiterario { get; set; }
}
' VB.NET
Public Class Livro
Public Property ID As Integer
Public Property Nome As String
Public Property GeneroLiterario As String
Public Property TipoGeneroLiterario As String
End Class
Uma vez adicionada a classe, compile o projeto. Esse é um passo muito importante, uma vez que, caso esqueçamos de compilar o projeto antes de criarmos o relatório, o mecanismo do Report Viewer não reconhecerá essa classe.
Com o projeto compilado, vamos adicionar um novo relatório do Report Viewer, dando o nome de “RelatorioListaLivros” para esse novo relatório. Dentro do relatório, adicione uma nova Table. Quando adicionamos uma Table no relatório, o Report Viewer perguntará informações sobre a fonte de dados que deverá ser utilizada para alimentar essa Table. No nosso caso, vamos criar uma nova fonte de dados do tipo “Object Data Source“:
Na próxima tela, encontre a classe “Livro” e conclua o assistente:
Em seguida, dê o nome de “DataSetLivro” para o DataSet que será criado:
Por fim, arraste os campos do DataSet para dentro das colunas da Table, de forma que o relatório fique parecido com a imagem abaixo:
Nada demais, não é mesmo? Vamos exibir esse relatório em tempo de execução para conferirmos o resultado? Para isso, abra o designer do formulário e arraste um controle do Report Viewer para dentro dele. Na smart tag do controle, escolha o relatório que acabamos de criar e, no code-behind do formulário, vamos passar uma lista de livros como fonte de dados do relatório:
// C#
private void FormRelatorio_Load(object sender, EventArgs e)
{
var livros = new List<Livro>();
livros.Add(new Livro() { ID = 1, Nome = "Ilíada", GeneroLiterario = "Epopéia", TipoGeneroLiterario = "Narrativo" });
livros.Add(new Livro() { ID = 2, Nome = "A divina comédia", GeneroLiterario = "Epopéia", TipoGeneroLiterario = "Narrativo" });
livros.Add(new Livro() { ID = 3, Nome = "Memórias póstumas de Brás Cubas", GeneroLiterario = "Romance", TipoGeneroLiterario = "Narrativo" });
livros.Add(new Livro() { ID = 4, Nome = "Macunaíma", GeneroLiterario = "Romance", TipoGeneroLiterario = "Narrativo" });
livros.Add(new Livro() { ID = 5, Nome = "Morte e vida Severina", GeneroLiterario = "Poesia", TipoGeneroLiterario = "Lírico" });
livros.Add(new Livro() { ID = 6, Nome = "As vespas", GeneroLiterario = "Comédia", TipoGeneroLiterario = "Dramático" });
livros.Add(new Livro() { ID = 7, Nome = "O misantropo", GeneroLiterario = "Comédia", TipoGeneroLiterario = "Dramático" });
livros.Add(new Livro() { ID = 8, Nome = "Auto da compadecida", GeneroLiterario = "Drama", TipoGeneroLiterario = "Dramático" });
this.reportViewer1.LocalReport.DataSources.Add(new Microsoft.Reporting.WinForms.ReportDataSource("DataSetLivro", livros));
this.reportViewer1.RefreshReport();
}
' VB.NET
Private Sub FormRelatorio_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim Livros As New List(Of Livro)
Livros.Add(New Livro() With {.ID = 1, .Nome = "Ilíada", .GeneroLiterario = "Epopéia", .TipoGeneroLiterario = "Narrativo"})
Livros.Add(New Livro() With {.ID = 2, .Nome = "A divina comédia", .GeneroLiterario = "Epopéia", .TipoGeneroLiterario = "Narrativo"})
Livros.Add(New Livro() With {.ID = 3, .Nome = "Memórias póstumas de Brás Cubas", .GeneroLiterario = "Romance", .TipoGeneroLiterario = "Narrativo"})
Livros.Add(New Livro() With {.ID = 4, .Nome = "Macunaíma", .GeneroLiterario = "Romance", .TipoGeneroLiterario = "Narrativo"})
Livros.Add(New Livro() With {.ID = 5, .Nome = "Morte e vida Severina", .GeneroLiterario = "Poesia", .TipoGeneroLiterario = "Lírico"})
Livros.Add(New Livro() With {.ID = 6, .Nome = "As vespas", .GeneroLiterario = "Comédia", .TipoGeneroLiterario = "Dramático"})
Livros.Add(New Livro() With {.ID = 7, .Nome = "O misantropo", .GeneroLiterario = "Comédia", .TipoGeneroLiterario = "Dramático"})
Livros.Add(New Livro() With {.ID = 8, .Nome = "Auto da compadecida", .GeneroLiterario = "Drama", .TipoGeneroLiterario = "Dramático"})
Me.ReportViewer1.LocalReport.DataSources.Add(New Microsoft.Reporting.WinForms.ReportDataSource("DataSetLivro", Livros))
Me.ReportViewer1.RefreshReport()
End Sub
Execute o projeto e veja o resultado:
Nota: me perdoe se os gêneros literários desse exemplo estiverem errados. Tudo o que eu lembro de literatura é o que eu estudei no colegial, lá por volta do ano 2000.
Agora você deve estar pensando: “OK, André, esse tipo de relatório eu estou cansado de fazer. Como é que eu faço a quebra de página baseada em uma regra de negócio? Não era isso que você ia mostrar nesse artigo?“.
Calma! Não se estresse. Acompanhe logo abaixo alguns tipos de quebra condicional que você pode adicionar nos seus relatórios do Report Viewer.
Quebrando página a cada N registros
O primeiro tipo de quebra condicional que eu quero mostrar neste artigo é a quebra de página a cada “N” registros. Tomando como base o nosso exemplo, vamos supor que nós queiramos mostrar somente quatro livros por página. Como é que nós fazemos isso?
Como o Report Viewer não suporta a definição de uma expressão para quebra de páginas, a alternativa é criarmos um agrupamento baseado na expressão desejada e, em seguida, configurarmos a quebra de página entre instâncias desse agrupamento. Pareceu complicado? Não se preocupe. Até que é bem simples, veja só.
Em primeiro lugar, vamos adicionar um agrupamento no relatório. Para isso, vamos até a janela de “Row Groups“, clicamos no dropdown dos detalhes e escolhemos a opção “Add Group => Parent Group“:
E é na próxima tela que mora todo o segredo. Na tela de criação de agrupamentos, temos que clicar no botão “fx” para configurarmos a expressão que será utilizada para fazer a quebra de página:
A expressão para quebrarmos a página a cada 4 registros seria a seguinte:
=Math.Ceiling(RowNumber(Nothing) / 4)
Note que estamos utilizando a função RowNumber passando “Nothing“, que retornará o número do registro considerando o DataSet como um todo. Se quiséssemos o número da linha considerando um agrupamento, teríamos que passar o nome do agrupamento como parâmetro para a função RowNumber.
Além disso, estamos utilizando também a função Math.Ceiling, que retorna o inteiro mais próximo acima do valor passado. Essa função é tipo um arredondamento forçado para cima. Dessa forma, na primeira linha, teremos RowNumber 1 dividido por 4, resultando em 0,25, arredondado para cima = 1. O resultado será o mesmo para as linhas de 1 até 4. A partir da linha 5, o resultado começa a ser 2 (5 / 4 = 1,25 => arredondado para cima = 2). Com isso, teremos um valor em comum entre as linhas que queremos agrupar.
Um pequeno detalhe é que, após a criação do grupo, o Report Viewer criará automaticamente uma coluna na tabela com o seu valor. Essa coluna pode ser excluída, porém, temos que tomar cuidado para excluirmos somente a coluna (e não o agrupamento):
Agora só falta configurarmos a quebra de página entre as instâncias do grupo e nós teremos o resultado esperado. Para configurarmos a quebra de página, temos que ir até as propriedades do agrupamento e, dentro da categoria “Page Breaks“, temos que marcar a opção “Between each instance of a group“:
Pronto! Tente executar o projeto e receba este belo erro de compilação:
A sort expression for tablix ‘Tablix1’ uses the RowNumber function. RowNumber cannot be used in sort expressions.
Esse erro acontece porque, quando criamos um agrupamento no Report Viewer, ele automaticamente configura a expressão correspondente ao agrupamento como a sua expressão de ordenação. Porém, nesse caso, como estamos utilizando a função RowNumber, nós não podemos utilizar essa expressão na ordenação do agrupamento. Dessa forma, temos que ir até as propriedades do grupo e, dentro da categoria “Sorting“, temos que excluir a expressão de ordenação:
Agora sim! Execute o projeto e veja o resultado:
Como você pode perceber, agora temos quatro registros por página. Obviamente, para o layout ficar completo, teríamos que mover a linha de cabeçalho para dentro do cabeçalho do grupo (para que a linha de cabeçalho seja repetida em todas as páginas).
Quebra de página baseada em uma expressão customizada
Da mesma forma que configuramos uma expressão para quebrarmos a página a cada “N” registros, nós podemos configurar qualquer expressão que quisermos para controlarmos a quebra de página. Por exemplo, digamos que nós precisemos quebrar o relatório em livros do tipo “Dramáticos” e “Não Dramáticos” (livros dramáticos em uma página e livros não dramáticos em outra página). Nesse caso, nós poderíamos seguir o mesmo processo, só que utilizando a seguinte expressão:
Por fim, existe aquela famosa situação em que alguns clientes querem o relatório com a quebra de página e outros clientes querem o relatório sem a quebra de página. Como fazer nesse caso? Criar duas versões do relatório, uma com quebra e outra sem quebra? É claro que não! Nós podemos utilizar um parâmetro para controlar se a quebra de página está ativa ou não!
Para isso, primeiramente temos que criar um novo parâmetro no nosso relatório. Vamos chamar esse parâmetro de “AtivaQuebra“, configurando o tipo “Boolean” e a opção “Allow null value“:
Em seguida, temos que clicar no agrupamento (na janela “Row Groups“) e, na janela de propriedades, temos que navegar até a propriedade “Group => Page Break => Disabled” para configurarmos uma expressão para essa propriedade:
A expressão para essa propriedade deve ser a seguinte:
=Not Parameters!AtivaQuebra.Value
Após essa alteração, a quebra de páginas customizada só estará ativa caso nós passemos o valor “True” para esse parâmetro antes de exibirmos o relatório, caso contrário a quebra de páginas será ignorada. Isso pode ser feito no code-behind do formulário, antes da chamada de “RefreshReport“:
// C#
// Se comentarmos a linha abaixo, a quebra de páginas customizada será ignorada.
this.reportViewer1.LocalReport.SetParameters(new Microsoft.Reporting.WinForms.ReportParameter("AtivaQuebra", "True"));
this.reportViewer1.RefreshReport();
' VB.NET
' Se comentarmos a linha abaixo, a quebra de páginas customizada será ignorada.
Me.ReportViewer1.LocalReport.SetParameters(New Microsoft.Reporting.WinForms.ReportParameter("AtivaQuebra", "True"))
Me.ReportViewer1.RefreshReport()
Concluindo
Quebra de página condicional no Report Viewer é algo nada intuitivo, mas, totalmente possível de ser feito. Através de criação de grupos com expressões customizadas, podemos criar quebras de páginas com qualquer regra de negócio que desejarmos. Nesse artigo você conferiu como quebrar o relatório a cada “N” registros, como configurar a quebra de páginas com expressões customizadas e como fazer para ativar ou desativar a quebra de página através de parâmetros.
E você, já precisou configurar uma quebra de página condicional no Report Viewer? Qual era a situação em que você teve que criar essa quebra de página customizada? Funcionou direitinho através da criação de agrupamentos? Conte-nos mais detalhes na caixa de comentários.
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Um erro clássico que acontece ao tentarmos exibir um relatório do Crystal Reports em aplicações que utilizam o .NET Framework 4 ou superior é o “FileNotFoundException” relacionado ao arquivo “crdb_adoplus.dll“. Como é que esse erro pode ser resolvido? A maneira mais simples e mais utilizada é fazermos um pequeno ajuste no arquivo app.config da nossa aplicação. Porém, essa não é a única maneira. Existe uma outra alternativa que faz muito sentido em alguns casos. No artigo de hoje você verá essas duas maneiras de resolver o erro do Crystal Reports FileNotFoundException crdb_adoplus.dll.
Entendendo o problema
Para entendermos esse problema, vamos criar um novo projeto do tipo “Windows Forms Application“. Dentro desse projeto, vamos adicionar um relatório do Crystal Reports em branco (vamos dar o nome de “Relatorio” para esse novo relatório que está sendo criado). Em seguida, no formulário, temos que arrastar um controle do Crystal Reports e selecionar o relatório que foi criado no passo anterior.
Ao executarmos essa aplicação, não recebemos erro algum:
Porém, vamos combinar que uma aplicação com um relatório em branco sem fonte de dados não tem nenhuma utilidade. Dessa forma, vamos criar uma classe muito simples que servirá de fonte de dados para o relatório. Essa classe se chamará “QualquerClasse” e terá somente uma propriedade (chamada “Propriedade“):
// C#
public class QualquerClasse
{
public int Propriedade { get; set; }
}
' VB.NET
Public Class QualquerClasse
Public Property Propriedade As Integer
End Class
Feito isso, no relatório, vamos até o Database Expert para arrastarmos essa classe para dentro do relatório:
A partir desse momento, ao executarmos novamente a nossa aplicação, o Crystal Reports mostrará a típica caixa de login que aparece quando não passamos a fonte de dados para o nosso relatório:
Isso faz todo o sentido, uma vez que temos uma tabela definida no nosso relatório e nós não estamos alimentando essa tabela ao exibirmos o relatório. OK, então vamos alimentar essa tabela com uma coleção de “QualquerClasse“. Essa coleção conterá somente uma instância dessa classe:
// C#
public FormRelatorio()
{
InitializeComponent();
Relatorio1.SetDataSource(new List<QualquerClasse>(new QualquerClasse[] { new QualquerClasse() { Propriedade = 1 } }));
}
' VB.NET
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Relatorio1.SetDataSource(New List(Of QualquerClasse)(New QualquerClasse() {New QualquerClasse() With {.Propriedade = 1}}))
End Sub
E é aí que surge o problema. Nesse ponto, ao executarmos novamente a nossa aplicação, receberemos esse belo erro:
An unhandled exception of type ‘System.IO.FileNotFoundException’ occurred in mscorlib.dll Additional information: Could not load file or assembly ‘file:///C:\Program Files (x86)\SAP BusinessObjects\Crystal Reports for .NET Framework 4.0\Common\SAP BusinessObjects Enterprise XI 4.0\win32_x86\dotnet1\crdb_adoplus.dll’ or one of its dependencies. The system cannot find the file specified.
Como mencionei no início do artigo, esse erro pode ser consertado de duas maneiras. Vamos conferir as duas opções?
Opção 1: useLegacyV2RuntimeActivationPolicy no app.config
A primeira opção é bem simples. Nós só temos que fazer um pequeno ajuste no arquivo app.config da nossa aplicação. Na tag “startup“, temos que definir o elemento “useLegacyV2RuntimeActivationPolicy” como “true“:
Se você quiser copiar e colar, segue o código da tag “startup” já com o ajuste desse elemento:
Execute novamente a aplicação e veja que o erro foi resolvido.
Opção 2: RuntimePolicyHelper
Em algumas situações, adicionarmos essa tag no arquivo app.config acaba sendo muito complicado. Isso acontece principalmente quando desmembramos a exibição dos relatórios em uma biblioteca (dll) separada. Nesse caso, seria o app.config da aplicação consumidora que deveria ser alterado, e isso é definitivamente algo que deve ser evitado ao disponibilizarmos uma biblioteca que pode ser consumida por várias aplicações consumidoras.
Na empresa onde eu trabalho nós tivemos exatamente esse problema. A lógica “genérica” de exibição de relatórios do Crystal Reports fica em uma dll separada, que é consumida por “N” aplicações. Nós queríamos evitar que o app.config de cada aplicação consumidora tivesse que ser alterado ao utilizar o Crystal Reports. Ao invés disso, nós queríamos fazer essa alteração do “legacy runtime” automaticamente quando a aplicação fosse executada.
Depois de pesquisar um pouquinho, encontramos uma classe “mágica“ que faz com que esse elemento seja adicionado em tempo de execução, evitando que o app.config tenha que ser alterado nas aplicações consumidoras. Vamos ver como essa classe funciona?
Primeiramente, vamos adicionar uma nova classe ao nosso projeto, dando o nome de “RuntimePolicyHelper“. Aqui vai o código dessa classe:
// C#
public static class RuntimePolicyHelper
{
public static bool LegacyV2RuntimeEnabledSuccessfully { get; private set; }
static RuntimePolicyHelper()
{
ICLRRuntimeInfo clrRuntimeInfo =
(ICLRRuntimeInfo)System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeInterfaceAsObject(
Guid.Empty,
typeof(ICLRRuntimeInfo).GUID);
try
{
clrRuntimeInfo.BindAsLegacyV2Runtime();
LegacyV2RuntimeEnabledSuccessfully = true;
}
catch (System.Runtime.InteropServices.COMException)
{
// This occurs with an HRESULT meaning
// "A different runtime was already bound to the legacy CLR version 2 activation policy."
LegacyV2RuntimeEnabledSuccessfully = false;
}
}
[System.Runtime.InteropServices.ComImport]
[System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown)]
[System.Runtime.InteropServices.Guid("BD39D1D2-BA2F-486A-89B0-B4B0CB466891")]
private interface ICLRRuntimeInfo
{
void xGetVersionString();
void xGetRuntimeDirectory();
void xIsLoaded();
void xIsLoadable();
void xLoadErrorString();
void xLoadLibrary();
void xGetProcAddress();
void xGetInterface();
void xSetDefaultStartupFlags();
void xGetDefaultStartupFlags();
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.InternalCall, MethodCodeType = System.Runtime.CompilerServices.MethodCodeType.Runtime)]
void BindAsLegacyV2Runtime();
}
}
' VB.NET
Public NotInheritable Class RuntimePolicyHelper
Public Shared Property LegacyV2RuntimeEnabledSuccessfully() As Boolean
Get
Return m_LegacyV2RuntimeEnabledSuccessfully
End Get
Private Set
m_LegacyV2RuntimeEnabledSuccessfully = Value
End Set
End Property
Private Shared m_LegacyV2RuntimeEnabledSuccessfully As Boolean
Shared Sub New()
Dim clrRuntimeInfo As ICLRRuntimeInfo = DirectCast(System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeInterfaceAsObject(Guid.Empty, GetType(ICLRRuntimeInfo).GUID), ICLRRuntimeInfo)
Try
clrRuntimeInfo.BindAsLegacyV2Runtime()
LegacyV2RuntimeEnabledSuccessfully = True
Catch generatedExceptionName As System.Runtime.InteropServices.COMException
' This occurs with an HRESULT meaning
' "A different runtime was already bound to the legacy CLR version 2 activation policy."
LegacyV2RuntimeEnabledSuccessfully = False
End Try
End Sub
<System.Runtime.InteropServices.ComImport>
<System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown)>
<System.Runtime.InteropServices.Guid("BD39D1D2-BA2F-486A-89B0-B4B0CB466891")>
Private Interface ICLRRuntimeInfo
Sub xGetVersionString()
Sub xGetRuntimeDirectory()
Sub xIsLoaded()
Sub xIsLoadable()
Sub xLoadErrorString()
Sub xLoadLibrary()
Sub xGetProcAddress()
Sub xGetInterface()
Sub xSetDefaultStartupFlags()
Sub xGetDefaultStartupFlags()
<System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.InternalCall, MethodCodeType:=System.Runtime.CompilerServices.MethodCodeType.Runtime)>
Sub BindAsLegacyV2Runtime()
End Interface
End Class
A utilização dessa classe é muito simples. Nós só temos que acessar a propriedade “LegacyV2RuntimeEnabledSuccessfully“, que retornará “true” caso o elemento tenha sido adicionado no startup com sucesso e “false” caso contrário. No nosso exemplo, como a exibição dos relatórios está sendo feita diretamente no projeto da aplicação, o lugar ideal para adicionarmos essa chamada seria no método “Main“:
// C#
[STAThread]
static void Main()
{
if (RuntimePolicyHelper.LegacyV2RuntimeEnabledSuccessfully)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FormRelatorio());
}
else
{
MessageBox.Show("Não foi possível ativar o LegacyV2Runtime para a aplicação.", "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
No VB.NET, por padrão, você não encontrará um método “Main” em projetos do tipo “Windows Forms Application“. Nesse caso, ou você altera as propriedades do projeto de forma que ele tenha um método “Main” (como descrito nesta thread do StackOverflow) ou você coloca a chamada dessa propriedade no construtor do formulário onde o relatório está sendo exibido:
' VB.NET
Public Sub New()
If (Not RuntimePolicyHelper.LegacyV2RuntimeEnabledSuccessfully) Then
MessageBox.Show("Não foi possível ativar o LegacyV2Runtime para a aplicação.", "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Relatorio1.SetDataSource(New List(Of QualquerClasse)(New QualquerClasse() {New QualquerClasse() With {.Propriedade = 1}}))
End Sub
Pronto! Com essas alterações, você não precisa mais definir o elemento no arquivo app.config. Ele será automaticamente adicionado em tempo de execução.
Concluindo
O disparo de uma “FileNotFoundException” é algo comum ao utilizarmos o controle do Crystal Reports em aplicações desenvolvidas com o .NET Framework 4 ou superior. Esse erro pode ser facilmente corrigido de duas maneiras: fazendo um ajuste no arquivo app.config ou adicionando dinamicamente um elemento nas políticas de runtime em tempo de execução. Neste artigo você conferiu as duas maneiras de resolver esse problema.
E você, já passou por esse problema com o Crystal Reports? Conhecia a segunda metodologia de resolução? Qual das duas opções você achou melhor? Conte-nos mais detalhes na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Você sabe as diferenças entre os bancos de dados locais SQLite, SQL Compact (CE) e LocalDb? E você sabe como utilizar cada um desses bancos de dados na sua aplicação? Um dos leitores desse site me fez justamente essa pergunta, que eu não sabia responder de “bate-pronto“, então, fui pesquisar. Depois de finalizar a minha pesquisa, cheguei à conclusão que ficaria impraticável escrever somente um artigo mostrando as diferenças de cada um desses bancos locais, além de dar exemplos de como utilizá-los. É um assunto muito extenso. Portanto, resolvi escrever uma série, primeiramente mostrando como utilizá-los e, por fim, um artigo mostrando as diferenças entre eles. Nesse primeiro artigo você vai ver como utilizar o SQLite no C# e VB.NET.
Criando um banco de dados SQLite
Uma das grandes vantagens dos bancos de dados locais é que eles são facilmente transportados e distribuídos com a aplicação. Normalmente, com esse tipo de banco de dados, tudo fica armazenado em um arquivo único, sem a necessidade de um servidor rodando por trás dele. Com o SQLite, toda a estrutura das tabelas, bem como os seus dados, ficam armazenados em um arquivo de extensão “.db“. Existem duas opções para criarmos os nossos bancos de dados do SQLite: via código ou utilizando ferramentas de administração.
Uma vez criado um novo arquivo com a extensão “.db“, a inicialização das tabelas via código pode ser feita via ADO.NET puro (com comandos do tipo “CREATE TABLE“) ou através do Entity Framework Code First (onde você tem as classes de domínio e o Entity Framework cria as tabelas automaticamente para você). No final desse artigo veremos como podemos utilizar o Code First com o SQLite (que não funciona “por padrão“). Não mostrarei a criação das tabelas via comandos SQL porque esse não é um cenário muito comum. Além disso, uma vez que você saiba utilizar o SQLite com o ADO.NET (que eu vou mostrar no artigo), você pode simplesmente adaptar o código para executar comandos “CREATE TABLE“.
Ao invés de mostrar a criação do banco via comandos SQL, resolvi mostrar uma ferramenta de administração de bancos SQLite que é bastante famosa: o DB Browser for SQLite. Essa ferramenta é open source e sua interface é bem simples e eficiente.
Uma vez instalado o DB Browser, ao abrir a aplicação, temos duas opções: criar um novo banco de dados ou abrir um banco de dados já existente. No nosso caso, como nós ainda não temos nenhum banco, vamos criar um novo arquivo, clicando na opção “New Database“:
Por padrão, ao criarmos um novo banco com o DB Browser, ele mostrará uma janela para criarmos a nossa primeira tabela. Poderíamos cancelar essa tela, mas, como realmente precisamos de uma tabela para fazer os nossos testes, vamos aproveitar para cria-la agora mesmo. Dê o nome de “Cliente” para a tabela sendo criada e adicione duas colunas: “Id” (chave primária, auto incremento) e “Nome” (texto, não nulo), conforme apresentado na imagem abaixo:
Note que, mesmo depois de termos criado a tabela, ela ainda está armazenada somente em memória. Para realmente salvarmos as alterações no arquivo, temos que clicar no botão “Write Changes“:
Como qualquer outro banco de dados, no SQLite podemos criar diversas tabelas e relacionamentos (através de “foreign keys“). O SQLite suporta até mesmo triggers (apesar de eu não gostar delas). Para não complicar muito o nosso exemplo, vamos trabalhar somente com essa única tabela.
Você pode conferir o conteúdo de cada tabela dentro da aba “Browse Data” do DB Browser. Obviamente, como acabamos de criar a tabela, ela estará vazia:
Adicionando as bibliotecas do SQLite via NuGet
Agora que já temos o nosso banco de dados criado, vamos conferir como podemos utilizá-lo nas nossas aplicações .NET. Para simplificar, vamos criar um novo projeto do tipo “Console Application“. Poderíamos baixar as dlls do SQLite diretamente no site oficial, porém, existe uma maneira muito mais simples de adicionarmos o SQLite no nosso projeto: através do NuGet!
Para isso, abra o Package Manager Console ou a janela de administração dos pacotes do NuGet, faça uma busca por SQLite e instale o pacote “System.Data.SQLite“:
Nota: caso você não saiba como o NuGet funciona, confira este artigo que eu escrevi sobre ele.
No meu caso, devido a algum problema na minha instalação do Visual Studio, eu não estava conseguindo instalar esse pacote. O Package Manager Console sempre me apresentava esse erro esquisito:
Depois de pesquisar bastante, eu encontrei esta thread no StackOverflow, sugerindo que eu apagasse o arquivo Nuget.config da pasta AppData. Entretanto, essa solução não funcionou. Por sorte, acabei encontrando essa issue no GitHub do NuGet, onde uma pessoa sugeriu limpar o cache através da ferramenta de linha de comando do NuGet. E foi isso que acabou resolvendo o meu problema:
Numa outra oportunidade eu escrevo um artigo contando em detalhes esse problema que eu tive com o NuGet. Se você estiver passando pelo mesmo problema e não conseguir resolver, entre em contato comigo que eu tento te ajudar.
Acessando um banco SQLite com ADO.NET puro
Após instalarmos a biblioteca do SQLite no nosso projeto, acessar um banco de dados com ADO.NET puro é muito simples. Dentro do namespace System.Data.SQLite temos as classes usuais do ADO.NET, que são as implementações de DbConnection, DbCommand, DbDataReader, DbDataAdapter, etc. Com essas classes, conseguimos criar uma conexão com o banco para executarmos comandos SQL que, ou farão alguma alteração nos dados do banco, ou retornarão um conjunto de dados baseado em uma consulta.
Para exemplificarmos a utilização das classes ADO.NET do SQLite, vamos fazer as operações CRUD básicas (Create, Read, Update, Delete). Primeiramente, temos que abrir uma conexão passando a connection string. Você encontra os mais diversos padrões de connection strings no site www.connectionstrings.com. A string de conexão padrão do SQLite é muito simples: “Data Source=CAMINHO_DO_ARQUIVO;“. No nosso caso, vamos supor que o arquivo que criamos anteriormente tenha o nome de “banco.db” e que ele esteja presente no diretório “bin/debug” do projeto:
// C#
using (var conn = new System.Data.SQLite.SQLiteConnection("Data Source=banco.db;"))
{
conn.Open();
}
' VB.NET
Using Conn As New System.Data.SQLite.SQLiteConnection("Data Source=banco.db;")
Conn.Open()
End Using
Em seguida, vamos executar um comando para deletar todos os registros da tabela “Cliente“:
// C#
using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
{
comm.CommandText = "DELETE FROM Cliente";
comm.ExecuteNonQuery();
}
' VB.NET
Using Comm As New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "DELETE FROM Cliente"
Comm.ExecuteNonQuery()
End Using
Agora que a tabela está vazia, vamos criar um novo registro contendo o nome “Novo Cliente“:
// C#
using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
{
comm.CommandText = "INSERT INTO Cliente (Nome) VALUES ('Novo Cliente')";
comm.ExecuteNonQuery();
}
' VB.NET
Using Comm As New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "INSERT INTO Cliente (Nome) VALUES ('Novo Cliente')"
Comm.ExecuteNonQuery()
End Using
Até aqui, tudo bem tranquilo, não é mesmo? Só utilizamos uma instância de SQLiteCommand e chamamos o método ExecuteNonQuery. Agora, como é que fazemos para atualizar o nome desse cliente que acabamos de cadastrar? Primeiro temos que pegar o seu “Id” utilizando o método ExecuteScalar e depois efetuamos um comando de UPDATE, só que agora utilizando a funcionalidade de parâmetros:
// C#
using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
{
comm.CommandText = "SELECT MAX(Id) FROM Cliente";
var clienteId = comm.ExecuteScalar();
if (clienteId != null && clienteId != DBNull.Value)
{
comm.CommandText = "UPDATE Cliente SET Nome = 'Novo Cliente Alterado' WHERE Id = @Id";
comm.Parameters.AddWithValue("@Id", clienteId);
comm.ExecuteNonQuery();
}
}
' VB.NET
Using Comm As New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "SELECT MAX(Id) FROM Cliente"
Dim ClienteId = Comm.ExecuteScalar()
If (ClienteId IsNot Nothing And ClienteId IsNot DBNull.Value) Then
Comm.CommandText = "UPDATE Cliente SET Nome = 'Novo Cliente Alterado' WHERE Id = @Id"
Comm.Parameters.AddWithValue("@Id", ClienteId)
Comm.ExecuteNonQuery()
End If
End Using
Nota: se você não utiliza parâmetros nos seus comandos ADO.NET, pare agora mesmo com isso. Você corre vários riscos ao concatenar valores nas suas consultas. Veja este artigo onde eu expliquei as implicações disso, além de uma demonstração de como utilizar parâmetros com o ADO.NET.
Por fim, só ficou faltando a parte “R” do CRUD, que significa “Read“, ou seja, leitura. Como é que fazemos para retornar todos os registros da tabela de clientes para listarmos os seus nomes? Fácil: é só utilizar um comando SELECT. Porém, para iterarmos nos resultados, temos duas opções: a primeira delas é utilizarmos um DataReader e a segunda opção é preenchermos uma DataTable utilizando um DataAdapter. Veja a diferença no trecho de código abaixo:
// C#
using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
{
comm.CommandText = "SELECT * FROM Cliente";
Console.WriteLine("DataReader:");
using (var reader = comm.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine("Nome do Cliente: {0}", reader["Nome"]);
}
}
Console.WriteLine("DataAdapter:");
var adapter = new System.Data.SQLite.SQLiteDataAdapter(comm);
var dataTable = new System.Data.DataTable();
adapter.Fill(dataTable);
foreach (System.Data.DataRow row in dataTable.Rows)
{
Console.WriteLine("Nome do Cliente: {0}", row["Nome"]);
}
}
' VB.NET
Using Comm As New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "SELECT * FROM Cliente"
Console.WriteLine("DataReader:")
Using Reader = Comm.ExecuteReader()
While Reader.Read()
Console.WriteLine("Nome do Cliente: {0}", Reader("Nome"))
End While
End Using
Console.WriteLine("DataAdapter:")
Dim Adapter As New System.Data.SQLite.SQLiteDataAdapter(Comm)
Dim DataTable As New System.Data.DataTable()
Adapter.Fill(DataTable)
For Each Row As DataRow In DataTable.Rows
Console.WriteLine("Nome do Cliente: {0}", Row("Nome"))
Next
End Using
Pronto! Com isso nós vimos todas as operações de criação, leitura, atualização e remoção de registros com o SQLite utilizando ADO.NET puro. Veja só como ficou o código completo:
// C#
using (var conn = new System.Data.SQLite.SQLiteConnection("Data Source=banco.db;"))
{
conn.Open();
// DELETE
using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
{
comm.CommandText = "DELETE FROM Cliente";
comm.ExecuteNonQuery();
}
// INSERT
using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
{
comm.CommandText = "INSERT INTO Cliente (Nome) VALUES ('Novo Cliente')";
comm.ExecuteNonQuery();
}
// UPDATE
using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
{
comm.CommandText = "SELECT MAX(Id) FROM Cliente";
var clienteId = comm.ExecuteScalar();
if (clienteId != null && clienteId != DBNull.Value)
{
comm.CommandText = "UPDATE Cliente SET Nome = 'Novo Cliente Alterado' WHERE Id = @Id";
comm.Parameters.AddWithValue("@Id", clienteId);
comm.ExecuteNonQuery();
}
}
// SELECT
using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
{
comm.CommandText = "SELECT * FROM Cliente";
Console.WriteLine("DataReader:");
using (var reader = comm.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine("Nome do Cliente: {0}", reader["Nome"]);
}
}
Console.WriteLine("DataAdapter:");
var adapter = new System.Data.SQLite.SQLiteDataAdapter(comm);
var dataTable = new System.Data.DataTable();
adapter.Fill(dataTable);
foreach (System.Data.DataRow row in dataTable.Rows)
{
Console.WriteLine("Nome do Cliente: {0}", row["Nome"]);
}
}
}
' VB.NET
Using Conn As New System.Data.SQLite.SQLiteConnection("Data Source=banco.db;")
Conn.Open()
' DELETE
Using Comm As New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "DELETE FROM Cliente"
Comm.ExecuteNonQuery()
End Using
' INSERT
Using Comm As New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "INSERT INTO Cliente (Nome) VALUES ('Novo Cliente')"
Comm.ExecuteNonQuery()
End Using
' UPDATE
Using Comm As New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "SELECT MAX(Id) FROM Cliente"
Dim ClienteId = Comm.ExecuteScalar()
If (ClienteId IsNot Nothing And ClienteId IsNot DBNull.Value) Then
Comm.CommandText = "UPDATE Cliente SET Nome = 'Novo Cliente Alterado' WHERE Id = @Id"
Comm.Parameters.AddWithValue("@Id", ClienteId)
Comm.ExecuteNonQuery()
End If
End Using
' SELECT
Using Comm As New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "SELECT * FROM Cliente"
Console.WriteLine("DataReader:")
Using Reader = Comm.ExecuteReader()
While Reader.Read()
Console.WriteLine("Nome do Cliente: {0}", Reader("Nome"))
End While
End Using
Console.WriteLine("DataAdapter:")
Dim Adapter As New System.Data.SQLite.SQLiteDataAdapter(Comm)
Dim DataTable As New System.Data.DataTable()
Adapter.Fill(DataTable)
For Each Row As DataRow In DataTable.Rows
Console.WriteLine("Nome do Cliente: {0}", Row("Nome"))
Next
End Using
End Using
Execute a aplicação e abra o banco no DB Browser para ver o novo registro criado na tabela de Clientes.
Acessando um banco SQLite com Entity Framework
Na seção anterior nós vimos como é simples acessarmos o SQLite através do ADO.NET puro. Mas, e se utilizamos o Entity Framework nos nossos projetos? Como é que fica a questão do SQLite? Sem problema algum! Quando adicionamos a biblioteca do SQLite pelo NuGet, ele já adiciona o suporte ao Entity Framework. Tem alguns truquezinhos para que tudo funcione corretamente, mas, não se preocupe. É isso que eu vou mostrar para você agora.
Antes de tudo, temos que criar uma nova classe que representará a nossa entidade (Cliente). Essa classe será bem simples, conforme você pode conferir abaixo:
// C#
public class Cliente
{
public long Id { get; set; }
public string Nome { get; set; }
}
' VB.NET
Public Class Cliente
Public Property Id As Long
Public Property Nome As String
End Class
Atenção! Note que o tipo do campo “Id” deve ser “long”, e não “int”. Isso se deve ao fato que todas as colunas INTEGER no SQLite são consideradas como Int64 pelo Entity Framework. Portanto, precisamos sempre utilizar o tipo “long” para evitarmos problemas.
A segunda coisa que precisamos adicionar ao trabalharmos com o Entity Framework é o contexto. Adicione uma nova classe no projeto, dando o nome de “EfContext“. Essa classe tem que herdar do DbContext do Entity Framework e, dentro dela, temos que criar um DbSet de Clientes:
// C#
public class EfContext : System.Data.Entity.DbContext
{
public System.Data.Entity.DbSet<Cliente> Cliente { get; set; }
}
' VB.NET
Public Class EfContext
Inherits System.Data.Entity.DbContext
Public Property Cliente As System.Data.Entity.DbSet(Of Cliente)
End Class
Não sei se você está acostumado a utilizar o Entity Framework, mas, existem algumas maneiras de passarmos a string de conexão para o contexto. Para simplificar as coisas, a metodologia que eu vou utilizar neste artigo é a especificação da string de conexão diretamente no arquivo app.config.
Como o banco estará armazenado no mesmo diretório da aplicação, temos que adicionar o seguinte item dentro da tag “configuration” do arquivo app.config:
Note que o Entity Framework só conseguirá encontrar a nossa string de conexão se ela tiver exatamente o mesmo nome da nossa classe de contexto (“EfContext“).
Uma vez criada a classe de contexto, vamos utilizá-la para fazermos as operações CRUD na tabela de Clientes. A primeira coisa que temos que fazer é criarmos uma instância do contexto:
// C#
using (var contexto = new EfContext())
{
}
' VB.NET
Using Contexto = New EfContext()
End Using
Em seguida, como fizemos anteriormente com o ADO.NET puro, vamos deletar todos os clientes da tabela de clientes:
// C#
foreach (var c in contexto.Cliente)
contexto.Cliente.Remove(c);
contexto.SaveChanges();
' VB.NET
For Each C In Contexto.Cliente
Contexto.Cliente.Remove(C)
Next
Contexto.SaveChanges()
E aí começam os problemas do SQLite com o Entity Framework. Tente executar a aplicação com o trecho de código acima e receba esse belo erro:
Unable to determine the provider name for provider factory of type ‘System.Data.SQLite.SQLiteFactory’. Make sure that the ADO.NET provider is installed or registered in the application config.
Esse erro acontece porque o arquivo app.config não é devidamente alterado para que consigamos utilizar o SQLite com o Entity Framework. Ao adicionarmos o SQLite ao nosso projeto, ele até faz algumas alterações referentes à utilização do SQLite com o Entity Framework, mas, infelizmente, algumas configurações ficam faltando. Para que consigamos fazer o SQLite funcionar com o Entity Framework, temos que adicionar três chaves no arquivo app.config:
E tome cuidado porque essas chaves precisam estar exatamente nessa ordem. Dependendo da ordem que você as colocar, o projeto continuará não funcionando.
Veja como deve ficar a chave “entityframework/providers” do seu app.config:
E aqui temos a chave “system.data/DbProviderFactories“:
<DbProviderFactories>
<remove invariant="System.Data.SQLite" />
<remove invariant="System.Data.SQLite.EF6" />
<add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
<add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6" description=".NET Framework Data Provider for SQLite (Entity Framework 6)" type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" />
</DbProviderFactories>
Pronto. Com essas alterações nós conseguimos resolver o primeiro problema, ou seja, agora o Entity Framework conseguirá encontrar as informações necessárias para utilizarmos o SQLite. Porém, ao tentarmos executar o projeto novamente, receberemos um segundo erro:
Ao abrirmos os detalhes da Inner Exception, fica claro o motivo de estarmos recebendo esse erro:
O Entity Framework não está conseguindo encontrar a tabela chamada “Clientes“. Mas, a nossa tabela se chama “Cliente“, por que é que o Entity Framework está considerando “Clientes“? Simples: por padrão, o Entity Framework pluraliza os nomes das tabelas. Dessa forma, mesmo tendo dado o nome de “Cliente” para o DbSet, o Entity Framework pluralizará e considerará “Clientes” no seu lugar.
Para desativarmos a pluralização do Entity Framework, temos que fazer um “override” no método “OnModelCreating” do nosso contexto. Dentro desse método, nós encontramos e removemos a convenção da pluralização do modelo:
// C#
public class EfContext : System.Data.Entity.DbContext
{
public System.Data.Entity.DbSet<Cliente> Cliente { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention>();
}
}
' VB.NET
Public Class EfContext
Inherits System.Data.Entity.DbContext
Public Property Cliente As System.Data.Entity.DbSet(Of Cliente)
Protected Overrides Sub OnModelCreating(modelBuilder As Entity.DbModelBuilder)
MyBase.OnModelCreating(modelBuilder)
modelBuilder.Conventions.Remove(Of System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention)()
End Sub
End Class
Agora sim, finalmente, ao executarmos a aplicação, não teremos nenhum erro e a tabela “Cliente” ficará vazia, pois estamos deletando todos os registros dela. Com isso, podemos continuar com a próxima etapa das nossas operações CRUD: a inserção de um novo cliente. O código nesse caso é muito simples. Basta adicionarmos uma nova instância da classe “Cliente” no nosso DbSet de Clientes e, logo em seguida, ao chamarmos o método “SaveChanges” um novo cliente será adicionado no banco de dados:
Em seguida, temos o código para alterarmos um cliente. Nesse caso, temos que recuperar o cliente do nosso DbSet, fazer as alterações desejadas e, por fim, temos que chamar o método “SaveChanges” para persistir as alterações (estou utilizando o método “First” porque só temos um cliente cadastrado – em um cenário real, teríamos que fazer uma pesquisa no DbSet através do “Id” do cliente):
// C#
var cliente = contexto.Cliente.First();
cliente.Nome = "Novo Cliente EF Alterado";
contexto.SaveChanges();
' VB.NET
Dim Cliente = Contexto.Cliente.First()
Cliente.Nome = "Novo Cliente EF Alterado"
Contexto.SaveChanges()
Por fim, para listarmos as informações dos nossos clientes, basta percorrermos o DbSet com um laço “foreach“:
// C#
foreach (var c in contexto.Cliente)
{
Console.WriteLine("Nome do Cliente: {0}", c.Nome);
}
' VB.NET
For Each C In Contexto.Cliente
Console.WriteLine("Nome do Cliente: {0}", C.Nome)
Next
Veja só como fica o código completo:
// C#
using (var contexto = new EfContext())
{
// DELETE
foreach (var c in contexto.Cliente)
contexto.Cliente.Remove(c);
contexto.SaveChanges();
// INSERT
contexto.Cliente.Add(new Cliente() { Nome = "Novo Cliente EF" });
contexto.SaveChanges();
// UPDATE
var cliente = contexto.Cliente.First();
cliente.Nome = "Novo Cliente EF Alterado";
contexto.SaveChanges();
// SELECT
foreach (var c in contexto.Cliente)
{
Console.WriteLine("Nome do Cliente: {0}", c.Nome);
}
}
' VB.NET
Using Contexto = New EfContext()
' DELETE
For Each C In Contexto.Cliente
Contexto.Cliente.Remove(C)
Next
Contexto.SaveChanges()
' INSERT
Contexto.Cliente.Add(New Cliente() With {.Nome = "Novo Cliente EF"})
Contexto.SaveChanges()
' UPDATE
Dim Cliente = Contexto.Cliente.First()
Cliente.Nome = "Novo Cliente EF Alterado"
Contexto.SaveChanges()
' SELECT
For Each C In Contexto.Cliente
Console.WriteLine("Nome do Cliente: {0}", C.Nome)
Next
End Using
E o code first?
Uma das grandes maravilhas do Entity Framework é a funcionalidade chamada “Code First“. Com ela, nós podemos criar o banco de dados através das nossas classes de domínio. Com o SQL Server nós temos essa funcionalidade nativamente, mas, e com o SQLite? Nesse caso, o “Code First” não está disponível por padrão, mas, existe uma biblioteca que adiciona essa funcionalidade: a SQLite.CodeFirst.
Após adicionarmos essa biblioteca através do NuGet, podemos ativar o “Code First” no nosso contexto. Isso é muito simples de ser feito, basta adicionarmos uma linha no método “OnModelCreating” do nosso contexto:
Pronto! Só com a adição dessa linha de código o Entity Framework criará o banco de dados automaticamente baseado nas entidades do seu contexto (caso o banco ainda não exista).
Concluindo
O SQLite é um dos tipos de bancos de dados “portáteis” disponíveis atualmente (e, a propósito, um dos mais utilizados). Este artigo foi um guia definitivo da utilização do SQLite com C# e VB.NET, onde eu mostrei uma ferramenta de administração de bancos SQLite, a utilização do SQLite com ADO.NET puro, a utilização do SQLite com Entity Framework e, por fim, mostrei como podemos habilitar o “Code First” do Entity Framework em projetos que utilizam o SQLite.
Você já conhecia o esse banco de dados? Como é que você faz para acessar os dados desse banco na sua aplicação? Você uiliza ADO.NET puro ou Entity Framework? Conhecia todas essas bibliotecas que eu mostrei no artigo? Conte-nos mais sobre as suas experiências na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Quem diria que aquela ferramenta singela de geração de relatórios lançada em 2004 iria alcançar a sua sexta edição em 2016? O Reporting Services cresceu muito desde a sua primeira edição, que foi disponibilizada como um add-on do SQL Server 2000! Eu só fico impressionado que, mesmo no Reporting Services 2016, depois de tanta evolução, ele continue não suportando a justificação de texto, que é uma funcionalidade tão básica. Mas, vamos deixar essa discussão para uma próxima oportunidade.
O que eu quero mostrar para você hoje é o processo de download, instalação, configuração padrão e publicação do primeiro relatório com o Reporting Services 2016 Express. Como você vai perceber logo no início, o processo ficou um pouco mais tranquilo nessa nova versão.
Baixando o Reporting Services 2016 Express
Antes de baixar qualquer versão do SQL Server 2016 Express, atente-se para o fato que o Reporting Services faz parte do que a Microsoft chama de “Advanced Services” do SQL Server. Até a versão 2014 do SQL Server, se quiséssemos que o Reporting Services fosse instalado em conjunto como SQL Server, nós tínhamos que baixar explicitamente o pacote de instalação do SQL Server with advanced services. Porém, a Microsoft alterou um pouco essa sistemática no SQL Server 2016, juntando tudo em um pacote único de instalação.
Se você procurar no Google por “SQL Server 2016 Express“, você provavelmente encontrará a página onde você pode baixar o instalador. Porém, quero deixar uma dica muito interessante aqui para você. Uns tempos atrás o Scott Hanselman (que trabalha no time de plataforma web na Microsoft), cansado da dificuldade de baixar as versões Express do SQL Server, preparou uma página com o link de download do SQL Server Express, desde a edição 2008 até a edição 2016. Essa página está disponível no seguinte endereço:
Não sei você, mas, eu sempre gosto de instalar todas as minhas ferramentas de desenvolvimento em inglês. Na minha opinião, essa é a maneira mais fácil de conseguir resolver problemas quando eles acontecem. Por exemplo, é muito mais fácil encontrar a solução para um problema se você procurar pela mensagem de erro em inglês do que se você procurar pela mensagem em português. Mesmo se você não domina muito o inglês, eu recomendo que você se esforce para trabalhar com as versões em inglês das ferramentas de desenvolvimento.
Entretanto, se você realmente não tiver condições de entender a interface da ferramenta em inglês e quiser instalar a versão em português, não se preocupe. O SQL Server 2016 está disponível em português. Você só precisa se atentar para escolher o idioma correto na hora de baixar o pacote de instalação:
Pré-requisitos de software e hardware
Os pré-requisitos de software e hardware estão claramente definidos na página de download do instalador:
Note que os pré-requisitos oficiais para o SQL Server 2016 não são lá grande coisa. Hoje em dia praticamente qualquer laptop ou desktop que você tenha atenderá esses pré-requisitos. Ah, e não se esqueça das limitações do SQL Server Express! Não importa a quantidade de memória que você tiver no seu computador, ele só consumirá no máximo 1 GB de memória RAM e cada banco (arquivo mdf) estará limitado ao tamanho de 10 GB de armazenamento (inclusive o banco de dados do Reporting Services).
Instalação passo a passo
Uma vez baixado o “pacote de instalação“, você notará que, na realidade, ele não é um pacote de instalação em si. Ele é somente uma “casca” onde você escolhe o que você quer baixar e instalar do SQL Server. Na primeira página do instalador, escolha a opção “Custom“. Com essa opção nós teremos a oportunidade de indicar que queremos instalar o Reporting Services também:
Na próxima tela, você só precisa escolher um local onde os arquivos temporários de instalação serão armazenados. Escolha a pasta desejada ou deixe a opção padrão e clique em “Install“.
Após o download do instalador, o assistente de instalação do SQL Server perguntará o que você quer instalar. No nosso caso, nós queremos instalar uma nova instância do SQL Server, então, temos que escolher a primeira opção:
A instalação do SQL Server 2016 com o Reporting Services é muito simples. Eu não vou mostrar aqui neste artigo as capturas de cada tela da instalação porque grande parte dela é o típico “next, next, finish” das instalações de aplicativos Windows. Vou mostrar para você somente as telas onde você tem que prestar atenção para escolher as opções corretas do Reporting Services.
A primeira tela que você tem que prestar muita atenção é a etapa “Feature Selection“. Essa tela é importantíssima. Aqui você precisa ter 100% de certeza de marcar a opção “Reporting Services – Native“, senão, obviamente, o Reporting Services não será instalado!
Uma outra opção que você deve tomar cuidado é, na etapa “Reporting Services Configuration“, não esqueça de marcar a opção “Install and configure“. Ao fazer isso, o próprio instalador configurará o Reporting Services com as opções padrão. Caso você escolha a outra opção (Install only), você terá que configurar posteriormente o Reporting Services através do Reporting Services Configuration Manager. Eu só recomendo essa opção se você realmente souber o que você está fazendo:
URLs do Reporting Services
Dependendo do computador onde você está instalando o SQL Server, a instalação pode demorar um pouco para completar. Eu fiz um teste de instalação em uma máquina virtual do Azure e o processo de instalação levou uns 15 minutos para ser finalizado. Uma vez tendo instalado com as opções que eu demonstrei anteriormente, vamos abrir o Reporting Services Configuration Manager para verificarmos as URLs que estão sendo utilizadas pelo Reporting Services. Você conseguirá encontrar o ícone para o aplicativo de configuração do Reporting Services na pasta “SQL Server 2016” do menu iniciar:
A primeira coisa que temos que fazer ao abrirmos o Reporting Services Configuration Manager é conectarmos à nossa instância do Reporting Services. Normalmente o próprio aplicativo encontrará a instância instalada no computador local e fará uma sugestão para que você se conecte àquela instância. Se você tiver instalado o Reporting Services em um outro computador, você terá que alterar as informações de conexão nesta tela:
Uma vez conectado ao Reporting Services, conseguiremos alterar todas as configurações possíveis através dessa ferramenta. Não vou entrar em detalhes nesse artigo sobre cada uma das possíveis configurações (talvez mais para frente eu escreva um outro artigo detalhando todas as opções). O que eu quero mostrar para você são as URLs que o SQL Server está utilizando para o Reporting Services. A primeira URL é o endereço do serviço em si. Você encontra essa URL na aba “Web Service URL“:
Como você pode notar, por padrão, o SQL Server utiliza a porta 80 seguido de “ReportServer_” e o nome da instância do SQL Server (no nosso caso, “SQLEXPRESS“). Se você tentar abrir essa URL no browser, você se deparará com algo parecido com isto:
Apesar do conteúdo não ser nada útil, esse endereço é muito importante. Esse é o endereço que precisaremos utilizar quando quisermos publicar um relatório ou quando quisermos exibir um relatório no controle do Report Viewer diretamente de dentro de uma aplicação (desktop ou web).
A próxima URL que temos à nossa disposição é a URL do portal do Reporting Services. Nesse portal conseguimos visualizar os relatórios e data sources que estão publicados no Reporting Services. Você encontra esse endereço na aba “Web Portal URL” da ferramenta de configuração:
Mais uma vez, por padrão, o SQL Server utiliza a porta 80. Porém, dessa vez a URL é seguida de “Reports_” e o nome da instância do SQL Server. Ao abrirmos esse endereço no browser, teremos o novo portal do Reporting Services 2016:
Por enquanto o portal está vazio, mas, assim que formos adicionando relatórios e data sources no nosso servidor, nós conseguiremos acessá-los visualmente através dessa URL.
Criando e publicando relatórios para o Reporting Services 2016
Para criarmos relatórios do Reporting Services 2016, nós podemos utilizar as ferramentas compatíveis com as versões anteriores do Reporting Services (SQL Server 2012 Report Builder ou ferramentas de design dentro do Visual Studio 2015 com o pacote de Data Tools) ou nós podemos utilizar a versão mais nova do Report Builder (2016), que já é compatível com o Reporting Services 2016.
Se você optar pelo Report Builder 2016, através do portal do Reporting Services você encontra um link para baixa-lo:
Não vou detalhar a instalação do Report Builder porque ela é uma instalação completamente tranquila. É só seguir o típico “next, next, finish” das instalações de aplicativos Windows que tudo vai dar certo.
Na tela inicial do Report Builder, escolha a opção de “Table or Matrix Wizard” e crie um relatório qualquer apontando para uma fonte de dados que esteja à sua disposição. No meu caso, eu utilizei o famoso banco de dados de exemplo do SQL Server (Adventure Works):
Uma vez criado o relatório, temos que salvá-lo no Reporting Services. Nessa etapa temos que prestar muita atenção para utilizarmos a URL correta do servidor (que deve ser a “Web Service URL“):
Com a conexão estabelecida, podemos agora salvar o relatório (através do menu File -> Save):
Feito isso, se atualizarmos o portal do Reporting Services, veremos que agora temos um novo relatório paginado disponível para execução:
Concluindo
O Reporting Services 2016 é sexta geração da ferramenta de relatórios “server side” da Microsoft, que vem sendo disponibilizada em conjunto com o SQL Server desde a versão 2000. Neste artigo você conferiu como baixar e instalar o Reporting Services 2016 Express utilizando as configurações padrão, bem como quais são as URLs que temos que nos atentar ao publicarmos os nossos relatórios no Reporting Services. Essa nova versão do Reporting Services está carregada de novas funcionalidades, que eu pretendo demonstrar em outros artigos desta série.
Antes de me despedir, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Desde 2010 eu vinha conseguindo renovar o prêmio todos os anos. Com a renovação do ano passado foram 6 anos consecutivos fazendo parte do programa. Durante todos esses anos, consegui participar de alguns MVP Open Days e participei duas vezes do MVP Summit, na sede da Microsoft em Seattle.
Como eu documentei a minha chegada ao programa em 2010, resolvi também escrever esse pequeno post documentando a minha saída do programa.
A famosa ligação
Eu já tinha ouvido falar que quando você não consegue renovar o prêmio, o(a) MVP Lead entra em contato com você algumas semanas antes da data de renovação. Então, quando a Fernanda Saraiva me mandou um e-mail perguntando o meu telefone duas semanas antes da minha data de renovação, eu já imaginei que eu receberia a notícia que o meu prêmio não seria renovado. E foi dito e feito. Uma semana depois ela acabou entrando em contato comigo falando que o time tinha decidido não renovar o meu prêmio.
O baque não foi tão grande porque eu já estava esperando a notícia (porque ela tinha perguntado o meu telefone na semana anterior), mas, confesso que na hora bateu uma baita tristeza, principalmente porque eu achava que as minhas contribuições seriam suficientes para a renovação.
Por que meu MVP não foi renovado?
O motivo pelo qual eu não consegui renovar o prêmio nesse ciclo foi que (segundo a Fernanda) “a minha categoria de premiação está buscando mais diversidade e contribuições multi canais“. Ou seja, como a maioria das minhas contribuições foram artigos, o time acabou não renovando o meu prêmio.
E quais seriam esses outros canais de publicação que eu deveria ter me atentado? Alguns exemplos citados foram meetups, apresentações em eventos e compartilhamento de código (bibliotecas no NuGet, código no GitHub, etc).
Portanto, se você é MVP atualmente ou se você está se esforçando para receber o prêmio pela primeira vez, fique atento que a diversificação de canais pode ser um fator importante para a sua competência.
E agora André, o que muda?
Além do trabalhão que vai dar para encontrar todos os lugares onde eu menciono que sou MVP (assinaturas de e-mails, assinatura do fórum da MSDN, bio, etc), não teremos muitas mudanças por aqui.
Quem me conhece sabe que eu não publico conteúdo com o objetivo de ganhar MVP. Em primeiro lugar, eu gosto de ensinar, então, eu escrevo artigos porque eu simplesmente gosto de compartilhar conhecimento. E em segundo lugar, o meu objetivo de longo prazo é viver de publicação de conteúdo (livros, e-books, cursos, podcast, etc).
Dito isso, o tipo de conteúdo que você já está acostumado a ver aqui no meu site todas as quartas-feiras continuará sendo publicado normalmente. O que eu vou tentar fazer no próximo ano é começar a publicar um vídeo por semana no meu canal do Youtube. Vou tentar participar também de mais apresentações online (infelizmente não pude mais fazer palestras presenciais depois que me mudei do Brasil em 2011). Vamos ver se eu consigo voltar a fazer parte do programa com essas contribuições nesses novos canais de publicação.
Dicas para conseguir o MVP (ou não)
Ironicamente, no final do ano passado eu fiz uma transmissão no Periscope contando como foi o meu processo de chegada ao MVP onde eu também dei algumas dicas para quem está querendo alcançar o prêmio. Se você se interessar, dá uma olhada aqui (só não cometa o mesmo erro que eu cometi e diversifique os canais de publicação!!):
Pelo menos fiquei feliz que pessoas tão competentes como o Renato Groffe e o Joel Rodrigues finalmente conseguiram o prêmio depois de tanta contribuição com a comunidade! Menos mal. :)
“André, eu consigo criar relatórios do Crystal Reports sem DataSet nem classe?“. Resposta: consegue sim! Uns tempos atrás eu mostrei como imprimir dados de um DataGridView no Crystal Reports. Naquele artigo eu mostrei duas maneiras para definirmos a estrutura de dados do relatório: através de um DataSet e através de uma coleção de instâncias de uma classe. Mas, e se tivermos que desenhar o relatório antes mesmo que o DataSet ou classe tenham sido criados? Nesse caso, temos que recorrer aos arquivos TTX, que nada mais são que arquivos de texto contendo as definições dos campos de cada tabela do relatório. É justamente isso que eu vou mostrar para você no artigo de hoje. Vem comigo!
Criando um relatório com arquivo TTX
Primeiramente, vamos criar um novo relatório no nosso projeto. Só para simplificar as coisas, neste artigo eu vou utilizar um projeto do tipo “Windows Forms Application“. Você poderia utilizar exatamente a mesma sistemática com outros tipos de projetos, como WPF ou Web Forms.
Dentro desse projeto, adicione um novo relatório em branco, dando o nome de “RelatorioFuncionario“. Com o relatório criado, a primeira coisa que temos que fazer é definirmos a estrutura de dados do relatório. Normalmente, nessa etapa nós selecionaríamos um DataSet ou classe do nosso projeto que serviria como fonte de dados para o nosso relatório. Porém, como o objetivo desse artigo é justamente definir o relatório sem DataSet ou classe, nós teremos que criar a definição dos campos com um arquivo TTX.
Os arquivos TTX nada mais são que arquivos de texto que contém a definição de campos de uma tabela. Esse arquivo pode ser criado manualmente com um editor de texto, mas, essa não é a melhor maneira de fazermos isso. O próprio Crystal Reports tem um “gerador” de arquivos TTX. Para evitarmos problemas, vamos utilizar esse gerador do Crystal Reports.
Para acessarmos o gerador de arquivos TTX, nós temos que abrir a tela “Database Expert“:
Dentro da janela “Database Expert“, encontre o item “Field Definitions Only“, que está localizado dentro da categoria “Create New Connection” / “More Data Sources“. Clique no botão de expansão desse item:
Na janela “Field Definitions Only“, como ainda não temos nenhum arquivo TTX, vamos clicar na opção “Create File“:
Ao fazermos isso, a janela de “Database Definition Tool” será aberta. Nessa janela podemos adicionar os campos da nossa tabela, indicando os seus nomes, tipos de dados e até mesmo um dado de exemplo (opcional).
Neste artigo nós faremos um relatório bem simples de listagem de funcionários, portanto, vamos criar somente três campos. O campo FuncionarioID será do tipo numérico e os campos Nome e Sobrenome serão do tipo string. Note que para os campos do tipo string nós temos que definir a quantidade de caracteres. A quantidade máxima de caracteres dos campos string é 65534. Para qualquer campo que tenha um tamanho maior que esse, você terá que utilizar o tipo “Memo” ao invés de “String“:
Uma vez adicionados os campos, feche a janela. Ao fechar a janela o Crystal Reports perguntará o local onde você quer salvar esse novo arquivo TTX. Escolha o diretório desejado e clique em salvar. Atente-se para o fato que o nome do arquivo sempre corresponderá ao nome da tabela no relatório. Portanto, salve o arquivo com o nome de “Funcionario.ttx“, dessa forma o Crystal Reports utilizará o nome “Funcionario” para a tabela.
Com o arquivo criado e salvo, adicione a tabela “Funcionario” para a lista de tabelas do relatório:
Em seguida vamos arrastar os campos da tabela “Funcionario” para dentro do relatório e ele estará pronto para ser exibido na nossa aplicação:
E qual é a estrutura de um arquivo TTX?
Antes de prosseguirmos com a exibição do relatório, vamos dar uma olhada na estrutura do arquivo TTX. Se você abrir o arquivo que salvamos anteriormente em um editor de texto, você verá que o conteúdo dele será este:
FuncionarioID Number
Nome String 65534
Sobrenome String 65534
Note que a estrutura é muito simples. Em cada linha do arquivo temos a definição de uma coluna. A definição é composta pelo nome da coluna, tipo de dados, tamanho do campo e um dado de exemplo, tudo separado por “tabs“. Se por algum motivo você não quiser utilizar o editor de TTX do Crystal Reports, você pode criar ou editar esse arquivo “na mão” em um editor de texto sem problema algum.
Carregando o relatório com entidade anônima via LINQ
Agora que já temos o nosso relatório criado, vamos exibi-lo. Uns tempos atrás eu escrevi um artigo demonstrando como imprimir o conteúdo de um DataGridView no Crystal Reports. Nesse artigo eu mostrei dois tipos de carregamentos de dados nos relatórios do Crystal Reports: via DataSet ou via coleção de uma determinada classe. Só que eu esqueci que nós podemos enviar uma coleção de tipos anônimos gerados através de uma consulta LINQ! Então, eu vou mostrar para você neste artigo essa terceira maneira de enviar dados para o Crystal Reports: sem DataSet nem classe concreta.
Primeiramente, vamos adicionar o controle do Crystal Reports no nosso formulário e vamos escolher o relatório que criamos anteriormente:
Em seguida, vamos para o code-behind do formulário e, no construtor (ou no evento “Load” no caso do VB.NET), vamos criar um array com algumas informações de funcionários e, logo em seguida, vamos construir uma consulta LINQ que transformará esse array em uma coleção de um tipo anônimo com as colunas que precisamos no nosso relatório (FuncionarioID, Nome e Sobrenome). Por fim, a única coisa que fica faltando é passar essa coleção para o relatório e chamar um “Refresh” para os dados serem carregados:
// C#
public FormRelatorio()
{
InitializeComponent();
var funcionarios = new object[]
{
new object[] { 1, "André", "Lima" },
new object[] { 2, "Fulano", "de Tal" },
new object[] { 3, "Beltrano", "da Silva" },
};
var colecao = from object[] funcionario in funcionarios
select new
{
FuncionarioID = funcionario[0],
Nome = funcionario[1],
Sobrenome = funcionario[2]
};
RelatorioFuncionario1.SetDataSource(colecao);
RelatorioFuncionario1.Refresh();
}
' VB.NET
Private Sub FormRelatorio_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim Funcionarios As Object() =
{
New Object() {1, "André", "Lima"},
New Object() {2, "Fulano", "de Tal"},
New Object() {3, "Beltrano", "da Silva"}
}
Dim Colecao = From Funcionario In Funcionarios
Select New With
{
.FuncionarioID = Funcionario(0),
.Nome = Funcionario(1),
.Sobrenome = Funcionario(2)
}
RelatorioFuncionario1.SetDataSource(Colecao)
RelatorioFuncionario1.Refresh()
End Sub
E com isso, ao executarmos a nossa aplicação, veremos que os dados serão exibidos no relatório com sucesso:
Concluindo
Em algumas situações, pode ser que tenhamos que desenhar um relatório antes que o DataSet ou classe de dados tenham sido gerados. Nesse caso, se não quisermos criar um DataSet ou classe “fake“, nós temos a opção de criarmos arquivos TTX. Cada arquivo desse tipo contém a definição de campos de uma tabela que será utilizada no relatório do Crystal Reports.
Nesse artigo você conferiu como gerar relatórios do Crystal Reports sem DataSet nem classe, através de um arquivo TTX. Além disso, eu mostrei também como enviar dados para o relatório sem utilizar DataSets ou classes! Esse tipo de carregamento não se aplica a muitos cenários, mas, fica como dica para caso você precise na sua aplicação.
Antes de me despedir, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
É sempre a mesma coisa. A cada nova versão do Visual Studio surge aquele frio na barriga com relação à ferramenta de relatórios da Microsoft: será que o Report Viewer foi descontinuado? Isso aconteceu com bastante força no lançamento do Visual Studio 2015, quando a Microsoft decidiu remover o Report Viewer da instalação “típica” do Visual Studio, fazendo com que ele só aparecesse se você selecionasse o item “SQL Server Data Tools” durante a instalação do Visual Studio (inclusive eu mostro como fazer isso neste artigo).
Com a nova versão do Visual Studio (até o momento denominada de Visual Studio “15“), isso não poderia ser diferente. Nessa versão, a sensação é ainda pior. Mesmo instalando o “SQL Server Data Tools“, o Report Viewer ainda não aparece! Por que será? Teria a Microsoft realmente descontinuado o Report Viewer?
Não se assuste, até agora o Report Viewer não foi descontinuado pela Microsoft. Porém, a partir da próxima versão do Visual Studio, o modelo de distribuição dos controles e designers passarão por grandes mudanças. Confira o restante do artigo para ver os detalhes dessa mudança.
O novo modelo de distribuição do Report Viewer
Por ser o produto que eu escolhi focar nos últimos dois anos, eu tenho acompanhado de perto o que está acontecendo no mundo do Report Viewer / Reporting Services 2016. Até configurei uns alertas no Google para alguns termos relacionados a essas tecnologias. Dessa forma, sempre que algum novo artigo é indexado pelo Google, eu fico sabendo e vou lá dar uma olhada.
Foi através desse tipo de alerta que eu fiquei sabendo desta thread nós fóruns da MSDN americana. Nessa thread descobri qual será o novo modelo de distribuição do controle e do designer do Report Viewer / Reporting Services integrado ao Visual Studio.
Vejam só este trecho da thread, publicado pelo Brad Syrupta, engenheiro de software que trabalha no time de SQL Server BI na Microsoft:
Ou seja, o controle do Report Viewer não será mais distribuído em conjunto com o Visual Studio. A partir da próxima versão, ele será distribuído através de um pacote do NuGet. Essa é uma estratégia que a Microsoft tem utilizado com diversas outras tecnologias (como Entity Framework, por exemplo). Já a parte de designer dos relatórios será disponibilizada através de uma extensão do Visual Studio (VSIX), que será publicada na Visual Studio Gallery.
Qual o motivo da mudança?
Não podemos nos esquecer que a Microsoft é uma empresa gigantesca, formada por inúmeros times. Agora, vamos pensar somente na divisão de ferramentas para desenvolvimento de software (Visual Studio, .NET, etc). Já pensou a dificuldade que deve ser para sincronizar os diversos times envolvidos nesse processo?
Desde o Visual Studio 2005 até o Visual Studio 2015, o controle e o designer de relatórios do Report Viewer foram distribuídos juntamente com o Visual Studio. Isso traz um grande desafio para os times, uma vez que nem sempre as releases do Visual Studio estão sincronizadas com as releases do SQL Server (time por trás da experiência de relatórios RDL e RDLC).
Para tentar diminuir esse stress (entre outras coisas, como custos com recursos, manutenção, estratégias do produto, etc), a nova metodologia de distribuição fará com que as releases do controle do Report Viewer não estejam mais atreladas às releases do Visual Studio. Ou seja, nós não vamos mais precisar esperar que uma nova versão do Visual Studio seja lançada para podermos utilizar as novas versões do controle e do designer do Report Viewer.
Veja só este outro trecho do post do Brad Syrupta:
Testando a versão preview do controle do Report Viewer 2016
A primeira versão de testes do controle do Report Viewer 2016 foi lançada no dia 20 de setembro. Tanto o controle Windows Forms quanto o controle Web Forms estão disponíveis através de um pacote do NuGet. Para instalar o pacote, abra a janela de gerenciamento de pacotes do NuGet e procure por “reportviewercontrol“:
Outra opção é fazer a instalação do pacote através do Package Manager Console, utilizando estes nomes de pacotes (WinForms ou WebForms, dependendo do tipo do seu projeto):
Para mais informações sobre o gerenciamento de pacotes do NuGet através do Visual Studio, confira este artigo.
Para projetos Windows Forms, você não precisa fazer nenhum ajuste e o novo controle deve funcionar normalmente. Obviamente, se você já utilizava uma versão anterior do controle do Report Viewer, você precisa remover as referências antigas antes de adicionar o pacote da versão preview.
Para projetos web, se você já utilizava uma versão anterior do controle do Report Viewer, você terá que alterar algumas referências, apontando para a versão mais nova do controle (13.0.0.0). O primeiro lugar que você tem que alterar é a tag “Register” na página onde você utiliza o controle do Report Viewer:
Depois, temos que ajustar alguns pontos do arquivo web.config. O primeiro lugar é a referência para os assemblies (tag “assemblies“, dentro de “compilation“). Você deve assegurar-se que essas referências estejam apontando para a versão mais nova do controle:
Com essas alterações, o seu projeto deve rodar sem nenhum problema de compilação.
Resultados Windows Forms
Depois de testar o controle do Windows Forms, eu não consegui detectar nenhuma diferença visual no controle. Também fiz um teste de performance com um relatório contendo um milhão de registros e não consegui notar nenhuma diferença na velocidade no processamento. Veja só a “cara” do controle, que é idêntica à da versão anterior:
Resultados Web Forms
Por outro lado, o controle Web Forms foi completamente reformulado. Pelo que andei lendo, a Microsoft decidiu abandonar de vez a utilização de ActiveX para a impressão dos relatórios e está renderizando todo o conteúdo em HTML5. Porém, no relatório que eu testei as coisas não funcionaram muito bem:
Veja só o mesmo relatório sendo exibido sem problema algum na versão anterior do Report Viewer:
Testei o mesmo relatório tanto no Google Chrome quanto no Internet Explorer e Edge. O resultado foi o mesmo. Tentei também adicionar a tag “meta” forçando que sites intranet não sejam renderizados em modo compatibilidade (como indicado na seção “Common issues” deste link), mas, também não tive sucesso.
Consertando possíveis problemas com o controle web
Depois de finalizar a escrita deste artigo, eu estava dando uma olhada no fórum de Reporting Services da MSDN americana e acabei encontrando esta thread. Seguindo as instruções apresentadas nela, a versão nova do controle web do Report Viewer funcionou corretamente:
O problema acontece basicamente porque a última versão do SQL Server Data Tools (instalada com o Visual Studio 2015 e seus updates) adiciona no GAC uma versão do novo controle que ainda não estava finalizada. E como as dlls do GAC têm mais prioridade do que as dlls presentes na pasta da aplicação, o IIS acaba carregando essa versão incorreta do GAC.
Para corrigirmos esse problema, precisamos remover as dlls problemáticas do GAC. Conseguimos remover dlls do GAC utilizando a ferramenta “gacutil“. Essa ferramenta fica armazenada na pasta “C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin“. Abra um prompt de comando elevado (como administrador), navegue até esse diretório e tente executar os seguintes comandos para remover todas as dlls problemáticas do Report Viewer do GAC:
Se esses comandos forem executados sem nenhum erro, o seu ambiente já estará pronto. Porém, no meu caso eu recebi um erro ao tentar remover as dlls do GAC:
Unable to uninstall: assembly is required by one or more applications
Depois de pesquisar um pouco sobre esse erro, acabei encontrando este artigo, que explica claramente que o problema é que essas dlls estão sendo referenciadas pelo Windows Installer no registro do Windows. Se isso também estiver acontecendo no seu ambiente, você terá que primeiramente remover as suas referências do Windows Installer no registro do Windows.
No meu computador, eu encontrei as referências na pasta “HKLM\SOFTWARE\Classes\Installer\Assemblies\Global“. Porém, pode ser que em algumas instalações as referências estejam em “HKCU\Software\Microsoft\Installer\Assemblies\Global“, portanto, é importante procurar nessas duas pastas do registro. Uma vez encontradas as chaves do Report Viewer, delete tudo o que tiver versão “13” (não esqueça de fazer um backup do registro antes de deletar as chaves!):
Em seguida, execute novamente os comandos para remover as dlls do GAC e veja que, dessa vez, os comandos funcionam com sucesso:
Concluindo
Sempre que uma nova versão do Visual Studio é lançada (ou está prestes a ser lançada), surgem os rumores que o Report Viewer esteja sendo descontinuado pela Microsoft. Até hoje, isso nunca se concretizou e espero que não se concretize num futuro próximo.
Como você conferiu neste artigo, por razões estratégicas, a Microsoft está alterando o modelo de distribuição do Report Viewer a partir da próxima versão do Visual Studio. Esse novo modelo não será mais dependente das releases do Visual Studio, ou seja, poderemos ter novas versões do controle e das ferramentas de design independentemente das datas de release do Visual Studio.
O que você achou dessa nova estratégia? E o que você achou do novo visual do controle web do Report Viewer, desenvolvido completamente em HTML5? Fico aguardando as suas opiniões na caixa de comentários.
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
A próxima versão do Visual Studio, até agora denominada Visual Studio “15” e que provavelmente se chamará Visual Studio 2017 (se a Microsoft continuar com a mesma estratégia de nomenclatura), está atualmente em Preview 5. Indiscutivelmente, o foco dessa versão tem sido a melhoria de performance nos mais diversos aspectos do Visual Studio, indo desde a experiência de instalação até a responsividade do editor, passando pelo tempo de carregamento do Visual Studio e tempo de carregamento das soluções. Tudo isso está sendo otimizado para que nós, desenvolvedores, não percamos tempo desnecessário e foquemos no que realmente nos interessa: programar.
Instalação mais rápida e limpa
A primeira experiência que temos com qualquer aplicação é com o seu instalador. Se o processo de instalação de um aplicativo apresenta problemas ou demora demais, nós já ficamos com um pé atrás. Esse é um comportamento natural dos seres humanos. Com certeza você já deve ter ouvido aquela expressão que “a primeira impressão é a que fica“, não é mesmo?
Pois bem. Se você já instalou qualquer edição do Visual Studio até a 2015, você sabe que o processo é demorado e toma um espaço considerável em disco. Não é à toa que a maior reclamação sobre o Visual Studio no Twitter diz respeito ao tempo de instalação e/ou o espaço em disco necessário.
Se para nós já era claro que a experiência de instalação do Visual Studio era demorada, imagina só para a Microsoft, que provavelmente ouve essa reclamação diariamente?
Para melhorar o tempo de download e instalação do Visual Studio, a Microsoft reconstruiu do zero o instalador para a próxima versão do Visual Studio:
Como você pode perceber, o novo instalador está dividido em “bundles“, que você deverá escolher dependendo das suas necessidades. Até o instalador do Visual Studio 2015, você tinha que escolher exatamente os itens que você gostaria de instalar, tendo que tomar cuidado para não esquecer um item que seja imprescindível nos seus projetos. Eu vejo muita gente escolhendo fazer a instalação completa por não saber exatamente o que escolher, justamente porque uma lista com 100 opções para selecionar deixa qualquer pessoa confusa.
Os “bundles” foram introduzidos para acabar com essa confusão. Seus nomes são mais claros e objetivos. Você desenvolve aplicativos desktop? Então é uma boa ideia marcar o item “.NET desktop development“. Você só trabalha com desenvolvimento web? Então provavelmente você não precisará desse “bundle“, mas sim, do “Web Development“.
Além disso, a Microsoft está criando agora a opção de instalação “Visual Studio Core Editor“, que instalará somente o editor, compiladores, ferramentas de debugging e suporte a controle de versão (TFS/Git). Essa instalação mínima só consumirá aproximadamente 325 Mb em disco, ao invés dos 6 Gb necessários para a instalação mínima do Visual Studio 2015.
Por fim, toda essa experiência de instalação ficará ainda melhor, porque a Microsoft otimizou o processo e, teoricamente, a instalação deverá demorar muito menos tempo do que a instalação do Visual Studio 2015. O Giovanni Bassi cronometrou o tempo de instalação do Visual Studio “15” e o resultado foi surpreendente: 2 minutos e meio para concluir a instalação do “Visual Studio Core Editor” e 13 minutos para concluir a instalação do Visual Studio com as ferramentas para desenvolvimento web. Muito melhor do que os tempos de instalação do Visual Studio 2015.
Mais para a frente eu também quero fazer um teste parecido, utilizando duas máquinas virtuais idênticas no Microsoft Azure, uma instalando o Visual Studio 2015 completo e outra instalando o Visual Studio “15” completo, ambas a partir da ISO (que ainda não está disponível para as versões de Preview do Visual Studio “15” – por isso que eu ainda não fiz o teste).
Se você quiser saber mais sobre o novo instalador da próxima versão do Visual Studio, confira estes links:
Melhorias de performance: inicialização e carregamento de soluções
Não é só a experiência de instalação que está sendo melhorada na próxima versão do Visual Studio. Como eu mencionei no início do post, a Microsoft está focando muito na melhoria de performance dos mais diversos aspectos do Visual Studio “15“.
Como você deve saber, a inicialização do Visual Studio pode ser um tanto quanto demorada em alguns computadores. Em especial, a primeira execução do Visual Studio (first launch) tende a ser bem demorada. Isso acontece porque vários caches são criados nessa primeira execução do Visual Studio (como MEF, extensões, caixa de ferramentas, fontes, etc).
No Visual Studio “15“, a Microsoft alterou a criação de várias dessas caches para um modelo assíncrono. O que não deu para ser feito assíncrono, ela alterou para um modelo “deferido“, ou seja, alguns componentes só serão carregados quando o Visual Studio perceber que você precisará deles (como a caixa de ferramentas e lista de fontes, por exemplo). Todas essas melhorias resultaram em uma primeira execução do Visual Studio “15” que é 3 vezes mais rápida do que o Visual Studio 2015.
Já quanto às outras inicializações, a Microsoft implementou a possibilidade do carregamento assíncrono e “on-demand” de extensões. Porém, eu posso estar errado, mas, pelo que eu entendi, isso não será automático. Cada autor de extensão deverá implementar esse suporte em cada uma das suas extensões. Dessa forma, nós ainda vamos demorar um pouco para sentirmos o efeito dessa melhoria. Até o momento, a Microsoft implementou o esquema de carregamento sob demanda para as extensões da Xamarin e do Python.
Outra melhoria que a Microsoft está implementando nesse aspecto é uma janela que mostrará quais extensões e janelas de ferramentas que estão afetando negativamente o carregamento do seu Visual Studio. Nessa janela você pode decidir o que fazer com esses itens (desativar a extensão/ferramenta ou mantê-la ativada mesmo assim):
Além disso, a Microsoft adicionou uma outra funcionalidade que melhorará o tempo de carregamento de soluções grandes. Essa nova funcionalidade está sendo chamada de “Lightweight Solution Load“. Essa opção não virá habilitada por padrão e você só deve ativá-la caso você trabalhe com soluções médias ou grandes (Tools -> Options -> Projects and Solutions -> General -> Lightweight Solution Load). Com essa funcionalidade ativada, ao carregar uma solução, você ainda conseguirá navegar pelo código (com “Navigate To“, “Go to definition“, “Find all references“, etc), aplicar refactorings e compilar a solução. Porém, algumas operações serão feitas somente sob demanda.
Veja só este vídeo que a Microsoft gravou comparando o tempo de abertura da solução do Roslyn (que é composta por centenas de projetos) no Visual Studio 2015 e Visual Studio “15“:
Para saber mais sobre essas melhorias do Visual Studio, confira estes links:
Não sei se você sabe, mas, o Visual Studio ainda é um executável 32 bits. Isso quer dizer que o seu processo é limitado a 4 Gb de memória RAM. Uma vez que o processo do Visual Studio ultrapassa esse limite, duas coisas podem acontecer: ou ele começa a não funcionar direito (começa a agir de modo “estranho“) ou ele trava por completo, dando um erro de “out of memory“. Você provavelmente já passou por essas situações. Todo desenvolvedor Microsoft sabe que, quando o Visual Studio começa a não funcionar direito, é hora de fechar e abrir de novo. Isso provavelmente acontece porque o processo estava chegando perto do limite de 4 Gb de memória.
Para melhorar essa situação na nova versão do Visual Studio, a Microsoft está separando várias ferramentas em sub-processos. Dessa forma, ela diminui a carga do processo principal (devenv.exe), reduzindo, consequentemente, a chance de o Visual Studio travar por falta de memória.
Um dos serviços que foram migrados para um processo separado foi o serviço que implementa a compatibilidade com o Javascript no Visual Studio (IntelliSense para o Javascript, navegação dentro de arquivos Javascript, etc), que é necessário em aproximadamente um terço das soluções.
Outro serviço substituído nessa otimização foi o suporte ao Git. A versão 2015 do Visual Studio utiliza internamente uma biblioteca chamada “libgit2” para implementar as funcionalidades do Git. Na nova versão do Visual Studio a Microsoft está substituindo essa biblioteca pelo uso direto do executável “git.exe“, o que obviamente reduziu a utilização de memória do processo principal, além de ter trazido também diversas melhorias de responsividade.
Por fim, a Microsoft está otimizando o carregamento de símbolos (arquivos pdb) durante o processo de debugging. Em algumas situações o Visual Studio estava carregando mais arquivos de símbolos do que era necessário, e isso foi corrigido.
Para mais informações sobre esse tema, confira este link:
Sabe quando tentamos digitar algum código no editor do Visual Studio e o editor parece que trabalha em “slow motion“? Sabe aquela impressão que enquanto estamos digitando a terceira palavra o editor ainda nem acabou de digitar a primeira palavra? Ou quando clicamos em alguma opção e o Visual Studio dá aquela travada até fazer o que deveria ser feito? Ou até mesmo quando clicamos no botão para iniciar e a aplicação e o modo de debugging demora a iniciar? Obviamente a Microsoft procura melhorar a responsividade dessas operações a cada nova versão do Visual Studio.
No Visual Studio “15” a Microsoft está removendo o processo host para aplicações WPF, Windows Forms e Console em tempo de debugging. Não sei se você já percebeu, mas, quando debugamos uma aplicação desse tipo, o Visual Studio inicia um processo com extensão “.vshost.exe“. Esse processo servia para deixar o próximo processo de debugging mais rápido. Porém, ele estava causando uma certa lerdeza quando clicávamos no botão para parar a execução da aplicação (inclusive para outros tipos de projetos que nem utilizavam esse host, como projetos ASP.NET, UWP, etc). Por isso a Microsoft decidiu removê-lo e otimizou o processo de debugging de forma que ele está mais rápido do que no Visual Studio 2015, e sem o problema da lerdeza ao clicarmos no botão para parar a execução da aplicação.
Além disso, a Microsoft melhorou também a performance do editor XAML, principalmente no que diz respeito à mudança de uma tab para outra. Muito do trabalho que era feito diretamente no editor será feito agora em uma thread em segundo plano, o que melhorou consideravelmente o tempo de resposta do editor.
Por fim, na janela “Manage Visual Studio Performance” nós encontraremos agora uma lista de extensões que estão possivelmente causando aquela sensação de lerdeza do editor (será que o ReSharper vai aparecer nessa lista?):
Para saber mais sobre as melhorias de responsividade no Visual Studio “15”, confira este link:
Não são só melhorias de performance que a Microsoft está implementando na próxima versão do Visual Studio. Além de tudo funcionar mais rápido, ela também está trazendo diversas novas funcionalidades muito interessantes. Dessas novas funcionalidades, duas me chamaram a atenção. A primeira delas é o fato que o IntelliSense agora está ficando mais inteligente, filtrando e selecionando automaticamente as opções disponíveis de acordo com o contexto. Ou seja, se você está chamando um método que recebe um tipo específico de objeto, o IntelliSense automaticamente selecionará e/ou filtrará as opções de forma que somente objetos do tipo esperado sejam exibidos na lista.
A segunda funcionalidade que me chamou a atenção foi o “Run to Click“. Sabe quando estamos debugando um código e queremos que a execução avance até uma certa linha? Nesse caso nós normalmente colocamos um breakpoint e apertamos “F5” para o programa rodar até aquele ponto. Com essa nova funcionalidade, nós não precisamos mais disso. Um novo botão será exibido no começo de cada linha de código, cuja função será fazer com que a execução corra até a linha escolhida. Veja só que legal o novo botãozinho verde com símbolo de “avançar” no editor de texto:
Para dar uma olhada em outras funcionalidades de produtividade que estão sendo adicionadas no Visual Studio “15“, acesse este link:
Só tome cuidado porque existem alguns problemas de compatibilidade com versões anteriores do Visual Studio “15“, que podem até mesmo fazer com que funcionalidades do Visual Studio 2015 parem de funcionar. Sugiro que você dê uma olhada neste post do Renato Groffe para saber exatamente quais são esses problemas de compatibilidade.
Concluindo
A nova versão do Visual Studio está trazendo diversas melhorias de performance, responsividade, redução na utilização de memória, além de várias novas funcionalidades que aumentarão a sua produtividade no desenvolvimento dos seus aplicativos. Neste post você conferiu uma lista completa de todas as novas funcionalidades e melhorias divulgadas até agora e já disponíveis no Preview 5. Não perca tempo, baixe agora mesmo e fique por dentro das novidades você também!
O que você achou das melhorias que estão por vir na próxima versão do Visual Studio? Deixe a sua opinião na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
De acordo com o ranking do site DB-Engines, o Oracle é atualmente o banco de dados relacional mais utilizado no mundo. Quando eu comecei a trabalhar na Savcor em 2008, eu lembro que a instalação e configuração do Oracle, bem como a instalação do provider .NET, eram desafiadores. Eram muitos detalhes que tínhamos que prestar atenção para que tudo funcionasse corretamente. E o PL/SQL Developer? Como eu tinha ódio daquela ferramenta.
Desde 2008 eu venho trabalhando somente com SQL Server nas aplicações que eu tenho desenvolvido. Mas, como sempre é bom ficar por dentro das mais diversas tecnologias (principalmente quando estamos falando do banco de dados relacional mais utilizado do mundo), eu resolvi dar uma olhada no processo da instalação, configuração, gerenciamento e utilização do Oracle com C# e VB.NET. Veja só o que eu aprendi.
Instalando o Oracle Database Express
Como eu não trabalho normalmente com o Oracle, eu não entendo muito do modelo de licenciamento dele, ou seja, não sei quanto custa a licença da versão Standard ou Enterprise e não consegui encontrar a informação no site (aliás, que site complicado, não?). Inclusive, se alguém tiver mais experiência nessa área, fala para gente como é que funciona lá na caixa de comentários (ou me manda um e-mail com os detalhes aí eu coloco as informações no artigo dando crédito para você).
Dito isso, ao invés de me aventurar com alguma edição mais profissional do Oracle, eu decidi utilizar a edição Oracle Database Express, que é gratuita. No site da Oracle, procure pelo item “Oracle Database 11g Express“:
Aceite os termos de licença e baixe a versão que fizer mais sentido para você:
A instalação é muito simples, no estilo “next, next, finish” das instalações de aplicativos Windows. Basicamente a única coisa que você precisa escolher é a senha do usuário administrador.
Uma vez que você for apresentado com esta tela, a instalação foi concluída com sucesso:
Logo de cara, vi que a instalação criou um ícone no desktop, além de algumas entradas no menu iniciar:
Porém, ao tentar executar a opção “Get Started with Oracle Database“, recebi essa bela mensagem de erro:
Que bela experiência pós-instalação, hein Oracle? Enfim, pesquisei rapidamente sobre esse erro e encontrei esta thread no StackOverflow. Seguindo as instruções, chamei a URL “http://localhost:8080/apex/f?p=4950” no browser e, aí sim, consegui abrir as opções de configuração do Oracle:
Muito bem, com isso eu tive a certeza que o banco de dados foi realmente instalado com sucesso e que ele estava rodando.
Limitações da edição Express
Antes de continuarmos com o artigo, vamos dar uma olhada nas limitações do Oracle Database Express. Como toda versão gratuita, é natural esperarmos que ela seja limitada de alguma forma.
Como podemos observar na Wikipedia, a primeira versão do Oracle Database Express (10g, lançada em 2005) tinha as seguintes limitações:
– Utiliza somente um CPU do computador onde estiver instalado – Somente um máximo de 1GB de memória RAM é utilizado pelo processo – Banco de dados limitado a 4GB de armazenamento
Depois de um tempo, a partir da versão 11g (lançada em 2011), a Oracle decidiu aumentar o limite de armazenamento do banco de dados para 11GB. Essas limitações são quase idênticas às limitações do SQL Server Express (com a única diferença que o SQL Server Express tem um limite de 10GB por database, ao invés de 11GB do Oracle). É claro que você não vai colocar o ERP da sua empresa rodando em uma versão gratuita do Oracle ou SQL Server (pelo menos não deveria), mas, mesmo com essas limitações, dá para rodar uns sistemas bem consideráveis com essa edição gratuita.
Ferramentas de administração
OK. Até agora nós já conseguimos instalar o banco de dados e entendemos as limitações da edição Express. E agora, cadê o correspondente ao “SQL Server Management Studio” do Oracle? Como eu falei anteriormente, um dos meus maiores pesadelos quando trabalhei com Oracle no passado foi a ferramenta PL/SQL Developer que, comparada ao SQL Server Management Studio, não chega nem perto (na minha opinião), além de ser paga. Então, resolvi pesquisar sobre ferramentas gratuitas de administração do Oracle e encontrei esta outra thread no StackOverflow, onde acabei descobrindo que as principais ferramentas são o Oracle SQL Developer (da própria Oracle) e o Database.NET.
Eu comecei dando uma olhada no Oracle SQL Developer e ele acabou servindo para o que eu estava precisando neste artigo (administração básica do banco de dados, criação de usuários, tabelas, etc). Dessa forma, vou utilizá-lo nesse artigo para criarmos o banco de dados. Se você estiver mais familiarizado com o Dababase.NET ou alguma outra ferramenta de administração, fique à vontade para utilizar a ferramenta que você preferir.
Atente-se para baixar a versão que já vem com o JDK, senão você precisará ter o JDK instalado no seu computador:
Uma coisa interessante é que nós não precisamos, de fato, instalar o Oracle SQL Developer. Ele é um arquivo zip que você pode descompactar no seu computador e executar o arquivo “sqldeveloper.exe” para abrir a aplicação.
Com a aplicação aberta, tentei criar uma nova conexão utilizando o usuário admin (SYS), mas, recebi um erro falando que só consigo conectar como “SYS” utilizando a role SYSDBA ou SYSOPER. Muito bem, o problema é que eu deixei a role “Default” na hora de conectar:
Basta mudar o valor no ComboBox “Role” para SYSDBA que a conexão funcionará normalmente (não sei porque o banco de dados já vem com umas 500 tabelas já criadas, mas, tudo bem – OK, acabei aprendendo depois que essas tabelas apareceram porque eu loguei com o usuário de sistema):
Para analisarmos o funcionamento do Oracle em uma aplicação de exemplo, vamos seguir a mesma estratégia que eu utilizei no artigo sobre SQLite e vamos criar uma nova tabela extremamente simples, chamada “Cliente“, contendo somente uma coluna “Id” (chave primária, auto-incremento) e “Nome“:
Onde é que nós definimos uma coluna como auto-incremento no Oracle? Pois bem. Se você não sabe, o Oracle não tem uma coluna do tipo “auto-incremento” como o SQL Server ou MySQL. No Oracle isso é feito através de uma funcionalidade chamada “sequence” em conjunto com uma trigger de inserção de registros na tabela. Felizmente, com o Oracle SQL Developer, nós não temos que criar tudo isso manualmente. Ao ativarmos o modo avançado, nós conseguimos encontrar a opção “Identity Column” na tela de criação de tabelas. Aí é só alterar o tipo para “Column Sequence” e a coluna terá o comportamento de auto-incremento:
Será que se a gente clicar em “OK” a criação dessa simples tabela vai funcionar? É claro que não! Para não perder o costume, receberemos esse belo erro:
Como você pode perceber pela mensagem de erro, o Oracle não permite a criação de triggers em objetos que são do usuário de sistema. E, como eu mencionei anteriormente, a funcionalidade de auto-incremento no Oracle é implementada através de uma “sequence” em conjunto com uma trigger. E agora? O que fazemos? Pesquisando sobre esse erro, eu descobri nessa thread do StackOverflow que temos que criar um outro usuário para podermos fazer a criação dos objetos no nosso banco de dados (o que faz sentido – a criação de objetos no banco de dados com o usuário de sistema não é recomendada em nenhuma ferramenta de banco de dados).
Primeiramente, vamos deletar a tabela “CLIENTE” que foi criada pela ferramenta. Pois é. Mesmo tendo recebido o erro da trigger, a tabela foi criada:
Em seguida, criamos um novo usuário, clicando com o botão direito em “Other users” e escolhendo a opção “Create User“:
Eu dei o nome de “dbuser” para o novo usuário e adicionei todas as permissões para ele:
Obviamente, em um servidor de verdade você deveria criar o usuário somente com as permissões que ele deveria ter, mas, para fins didáticos neste artigo, eu configurei permissão total para o usuário.
Uma vez criado o usuário, vamos alterar a conexão para que ela não utilize mais o usuário de sistema, mas sim, esse usuário que acabamos de criar. Para isso, nós precisamos primeiramente desconectar do servidor:
Depois, nas propriedades da conexão, nós alteramos o usuário e senha:
Pronto! Agora se você seguir os mesmos passos que seguimos anteriormente para criarmos a tabela de Cliente, tudo vai funcionar sem nenhum erro:
Instalando o provider e suporte ao Visual Studio do Oracle
Ufa! Agora que já conseguimos finalmente criar a nossa singela tabela de Clientes, vamos partir para o desenvolvimento. Porém, antes disso, temos que instalar o provider e as ferramentas de suporte ao Visual Studio. Essas ferramentas podem ser baixadas diretamente no site da Oracle:
Eu recomendo a opção “ODAC + Developer Tools“. Essa é a opção mais completa, que instalará também o provider do Oracle para .NET, além do suporte ao Oracle no Visual Studio:
A instalação é bem tranquila. A única etapa que você tem que prestar atenção é o “DB Connection Configuration“. Nessa etapa o instalador perguntará os dados do seu servidor para que ele possa criar automaticamente as entradas no arquivo “tnsnames.ora” (se você não sabe, uma das formas para se conectar a um banco Oracle é utilizar o nome da entrada nesse arquivo na string de conexão – veremos mais informações sobre isso nas próximas seções deste artigo):
Acessando o Oracle com ADO.NET puro
Uma vez instalado o provider e ferramentas de suporte ao Visual Studio do Oracle, podemos partir para o desenvolvimento do nosso código de exemplo. Vamos ver como fazemos para implementarmos um CRUD (create, read, update, delete) bem básico em cima da tabela de Clientes?
Para simplificar as coisas, vou criar um novo projeto do tipo “Console Application“. Com o projeto criado, vamos adicionar a referência para a dll que implementa o suporte ao ADO.NET para o Oracle, que está localizada dentro da categoria “Extensions“:
Adicionada essa referência, nós já conseguimos trabalhar com as famosas classes do ADO.NET para o Oracle (OracleConnection, OracleCommand, OracleDataAdapter, etc). Todas essas classes estão localizadas no namespace “Oracle.DataAccess.Client“. A primeira coisa que vamos fazer é instanciar uma conexão e abri-la:
// C#
using (var conn = new Oracle.DataAccess.Client.OracleConnection("Data Source=dbserver;User Id=dbuser;Password=dbuser;"))
{
conn.Open();
}
' VB.NET
Using Conn As New Oracle.DataAccess.Client.OracleConnection("Data Source=dbserver;User Id=dbuser;Password=dbuser;")
Conn.Open()
End Using
Note que eu utilizei as informações criadas no arquivo “tnsnames.ora“, ou seja, o nome da entrada TNS escolhida durante a instalação das ferramentas de suporte ao Visual Studio do Oracle. Até uns tempos atrás, essa era a única opção para nos conectarmos a um banco de dados Oracle. Porém, felizmente, a partir de uma certa versão do Oracle (que eu não sei ao certo), nós podemos especificar todas as informações do banco de dados diretamente na string de conexão, sem termos que nos preocupar com o arquivo “tnsnames.ora“. Você encontra mais informações sobre essas strings de conexão aqui e aqui. No meu caso, a string de conexão sem utilizar o TNS ficou assim:
// C#
using (var conn = new Oracle.DataAccess.Client.OracleConnection("Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=XE)));User ID=dbuser;Password=dbuser;"))
{
conn.Open();
}
' VB.NET
Using Conn As New Oracle.DataAccess.Client.OracleConnection("Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=XE)));User ID=dbuser;Password=dbuser;")
Conn.Open()
End Using
Agora que já temos a nossa conexão instanciada e aberta, podemos começar a criar comandos para efetuarmos as nossas operações CRUD. A primeira operação que faremos é a exclusão de todos os dados da tabela de Clientes:
// C#
using (var comm = new Oracle.DataAccess.Client.OracleCommand())
{
comm.Connection = conn;
comm.CommandText = "DELETE FROM Cliente";
comm.ExecuteNonQuery();
}
' VB.NET
Using Comm As New Oracle.DataAccess.Client.OracleCommand()
Comm.Connection = Conn
Comm.CommandText = "DELETE FROM Cliente"
Comm.ExecuteNonQuery()
End Using
Note que essa operação é extremamente simples. Ela segue exatamente o mesmo padrão de execução de comandos ADO.NET com outros bancos de dados (SQL Server, Access, MySQL, SQLite, etc).
A próxima operação que analisaremos é a inserção de dados na tabela de Clientes. Faremos a inserção de uma nova entrada utilizando um valor fixo para a coluna “Nome“:
// C#
using (var comm = new Oracle.DataAccess.Client.OracleCommand())
{
comm.Connection = conn;
comm.CommandText = "INSERT INTO Cliente (Nome) VALUES ('Novo Cliente')";
comm.ExecuteNonQuery();
}
' VB.NET
Using Comm As New Oracle.DataAccess.Client.OracleCommand()
Comm.Connection = Conn
Comm.CommandText = "INSERT INTO Cliente (Nome) VALUES ('Novo Cliente')"
Comm.ExecuteNonQuery()
End Using
Em seguida, faremos a atualização do registro que acabamos de criar. Para isso, precisamos primeiramente descobrir qual foi o “Id” utilizado no registro que acabamos de criar. Descobriremos isso fazendo uma consulta pelo maior “Id” da tabela Cliente através de um “ExecuteScalar“. Com o “Id” em mãos, fazemos então a alteração no registro:
// C#
using (var comm = new Oracle.DataAccess.Client.OracleCommand())
{
comm.Connection = conn;
comm.CommandText = "SELECT MAX(Id) FROM Cliente";
var clienteId = comm.ExecuteScalar();
if (clienteId != null && clienteId != DBNull.Value)
{
comm.CommandText = "UPDATE Cliente SET Nome = 'Novo Cliente Alterado' WHERE Id = :Id";
comm.Parameters.Add("Id", clienteId);
comm.ExecuteNonQuery();
}
}
' VB.NET
Using Comm As New Oracle.DataAccess.Client.OracleCommand()
Comm.Connection = Conn
Comm.CommandText = "SELECT MAX(Id) FROM Cliente"
Dim ClienteId = Comm.ExecuteScalar()
If (ClienteId IsNot Nothing And ClienteId IsNot DBNull.Value) Then
Comm.CommandText = "UPDATE Cliente SET Nome = 'Novo Cliente Alterado' WHERE Id = :Id"
Comm.Parameters.Add("Id", ClienteId)
Comm.ExecuteNonQuery()
End If
End Using
Note que, na parte da atualização, nós utilizamos a funcionalidade de parâmetros do ADO.NET. Diferente do SQL Server (onde os nomes dos parâmetros devem começar com arroba), os parâmetros nomeados no Oracle devem começar com “dois pontos” seguido do nome do parâmetro (por exemplo, “:Id“). Depois, na hora de adicionar os parâmetros no comando, basta utilizarmos o mesmo nome que definimos no meio da query (sem os dois pontos) e tudo deve funcionar corretamente. Confira o meu artigo sobre parâmetros do ADO.NET para saber porque você deve utilizar essa funcionalidade ao invés de concatenar valores nas suas sentenças.
Por fim, vamos conferir como podemos fazer para listarmos os registros de uma tabela. Com o ADO.NET puro, temos duas opções: DataReader ou DataAdapter. Veja só como fica o código:
// C#
using (var comm = new Oracle.DataAccess.Client.OracleCommand())
{
comm.Connection = conn;
comm.CommandText = "SELECT * FROM Cliente";
Console.WriteLine("DataReader:");
using (var reader = comm.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine("Nome do Cliente: {0}", reader["Nome"]);
}
}
Console.WriteLine("DataAdapter:");
var adapter = new Oracle.DataAccess.Client.OracleDataAdapter(comm);
var dataTable = new System.Data.DataTable();
adapter.Fill(dataTable);
foreach (System.Data.DataRow row in dataTable.Rows)
{
Console.WriteLine("Nome do Cliente: {0}", row["Nome"]);
}
}
' VB.NET
Using Comm As New Oracle.DataAccess.Client.OracleCommand()
Comm.Connection = Conn
Comm.CommandText = "SELECT * FROM Cliente"
Console.WriteLine("DataReader:")
Using Reader = Comm.ExecuteReader()
While Reader.Read()
Console.WriteLine("Nome do Cliente: {0}", Reader("Nome"))
End While
End Using
Console.WriteLine("DataAdapter:")
Dim Adapter As New Oracle.DataAccess.Client.OracleDataAdapter(Comm)
Dim DataTable As New System.Data.DataTable()
Adapter.Fill(DataTable)
For Each Row As DataRow In DataTable.Rows
Console.WriteLine("Nome do Cliente: {0}", Row("Nome"))
Next
End Using
E, com isso, completamos todas as operações de CRUD no Oracle utilizando ADO.NET puro. Veja como ficou o código completo:
// C#
using (var conn = new Oracle.DataAccess.Client.OracleConnection("Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=XE)));User ID=dbuser;Password=dbuser;"))
{
conn.Open();
// DELETE
using (var comm = new Oracle.DataAccess.Client.OracleCommand())
{
comm.Connection = conn;
comm.CommandText = "DELETE FROM Cliente";
comm.ExecuteNonQuery();
}
// INSERT
using (var comm = new Oracle.DataAccess.Client.OracleCommand())
{
comm.Connection = conn;
comm.CommandText = "INSERT INTO Cliente (Nome) VALUES ('Novo Cliente')";
comm.ExecuteNonQuery();
}
// UPDATE
using (var comm = new Oracle.DataAccess.Client.OracleCommand())
{
comm.Connection = conn;
comm.CommandText = "SELECT MAX(Id) FROM Cliente";
var clienteId = comm.ExecuteScalar();
if (clienteId != null && clienteId != DBNull.Value)
{
comm.CommandText = "UPDATE Cliente SET Nome = 'Novo Cliente Alterado' WHERE Id = :Id";
comm.Parameters.Add("Id", clienteId);
comm.ExecuteNonQuery();
}
}
// SELECT
using (var comm = new Oracle.DataAccess.Client.OracleCommand())
{
comm.Connection = conn;
comm.CommandText = "SELECT * FROM Cliente";
Console.WriteLine("DataReader:");
using (var reader = comm.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine("Nome do Cliente: {0}", reader["Nome"]);
}
}
Console.WriteLine("DataAdapter:");
var adapter = new Oracle.DataAccess.Client.OracleDataAdapter(comm);
var dataTable = new System.Data.DataTable();
adapter.Fill(dataTable);
foreach (System.Data.DataRow row in dataTable.Rows)
{
Console.WriteLine("Nome do Cliente: {0}", row["Nome"]);
}
}
}
' VB.NET
Using Conn As New Oracle.DataAccess.Client.OracleConnection("Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=XE)));User ID=dbuser;Password=dbuser;")
Conn.Open()
' DELETE
Using Comm As New Oracle.DataAccess.Client.OracleCommand()
Comm.Connection = Conn
Comm.CommandText = "DELETE FROM Cliente"
Comm.ExecuteNonQuery()
End Using
' INSERT
Using Comm As New Oracle.DataAccess.Client.OracleCommand()
Comm.Connection = Conn
Comm.CommandText = "INSERT INTO Cliente (Nome) VALUES ('Novo Cliente')"
Comm.ExecuteNonQuery()
End Using
' UPDATE
Using Comm As New Oracle.DataAccess.Client.OracleCommand()
Comm.Connection = Conn
Comm.CommandText = "SELECT MAX(Id) FROM Cliente"
Dim ClienteId = Comm.ExecuteScalar()
If (ClienteId IsNot Nothing And ClienteId IsNot DBNull.Value) Then
Comm.CommandText = "UPDATE Cliente SET Nome = 'Novo Cliente Alterado' WHERE Id = :Id"
Comm.Parameters.Add("Id", ClienteId)
Comm.ExecuteNonQuery()
End If
End Using
' SELECT
Using Comm As New Oracle.DataAccess.Client.OracleCommand()
Comm.Connection = Conn
Comm.CommandText = "SELECT * FROM Cliente"
Console.WriteLine("DataReader:")
Using Reader = Comm.ExecuteReader()
While Reader.Read()
Console.WriteLine("Nome do Cliente: {0}", Reader("Nome"))
End While
End Using
Console.WriteLine("DataAdapter:")
Dim Adapter As New Oracle.DataAccess.Client.OracleDataAdapter(Comm)
Dim DataTable As New System.Data.DataTable()
Adapter.Fill(DataTable)
For Each Row As DataRow In DataTable.Rows
Console.WriteLine("Nome do Cliente: {0}", Row("Nome"))
Next
End Using
End Using
E como fica com o Entity Framework?
Agora que já vimos como fazer as operações CRUD no Oracle utilizando ADO.NET puro, vamos ver como conseguimos obter o mesmo resultado utilizando Entity Framework?
Primeiramente, temos que adicionar a referência para a biblioteca que implementa o suporte ao Entity Framework no Oracle. Poderíamos adicionar a referência diretamente para a dll que foi instalada no nosso computador (ao instalarmos o provider e as ferramentas de suporte ao Visual Studio do Oracle, a dll do Entity Framework para o Oracle também é instalada). Porém, se fizermos isso, teremos que ajustar o nosso arquivo app.config por conta própria.
Para evitarmos esse ajuste manual, vamos adicionar a referência para essa biblioteca utilizando o NuGet. Ao fazermos isso, o arquivo app.config será ajustado automaticamente com algumas configurações padrão, poupando um pouco do nosso trabalho:
Nesse exemplo, vamos trabalhar com Code First. Ou seja, temos que criar as classes de domínio no nosso projeto e o Entity Framework criará automaticamente as tabelas necessárias. No nosso caso, já temos a tabela de Clientes criada no banco, então, o Entity Framework deverá simplesmente detectar a sua existência e considerá-la nas consultas. A nossa única classe de domínio será a classe “Cliente“:
// C#
public class Cliente
{
public long Id { get; set; }
public string Nome { get; set; }
}
' VB.NET
Public Class Cliente
Public Property Id As Long
Public Property Nome As String
End Class
Em seguida, temos que criar a nossa classe de contexto do Entity Framework:
// C#
public class EfContext : System.Data.Entity.DbContext
{
public System.Data.Entity.DbSet<Cliente> Cliente { get; set; }
}
' VB.NET
Public Class EfContext
Inherits System.Data.Entity.DbContext
Public Property Cliente As System.Data.Entity.DbSet(Of Cliente)
End Class
Com isso, nós já temos a base que precisamos para criar uma instância do contexto na nossa aplicação. Não esqueça de instanciar o contexto dentro de um bloco “using“. Dessa forma, ele será automaticamente descartado após a finalização do bloco:
// C#
using (var contexto = new EfContext())
{
}
' VB.NET
Using Contexto = New EfContext()
End Using
Se executarmos o nosso projeto após adicionarmos o código acima, nós receberemos o seguinte erro:
Nós recebemos esse erro porque nós não configuramos as informações do nosso banco de dados no arquivo app.config. Para que o nosso exemplo funcione sem dar erro, nós temos que alterar dois lugares do nosso arquivo app.config:
Primeiro, temos que ajustar a tag “dataSource“, onde o elemento “alias” deve conter o nome do nosso contexto (“EfContext“) e o elemento “descriptor” deve conter a nossa string de conexão. Além disso, temos que alterar a tag “connectionStrings“, onde o elemento “name” deve conter o nome do nosso contexto e o elemento “connectionString” deve conter a nossa string de conexão.
Veja só como ficou a minha tag “dataSource” após as alterações:
Feito isso, se executarmos novamente a nossa aplicação, não receberemos mais nenhum erro. Isso quer dizer que o Entity Framework conseguiu localizar o nosso banco de dados e o contexto foi inicializado com sucesso.
Agora, da mesma forma que fizemos com o ADO.NET puro, vamos implementar o primeiro passo do nosso CRUD, que será a exclusão de todos os dados da tabela de Clientes. Para isso, nós fazemos um “foreach” na tabela de Clientes, removendo todos os registros e salvando as alterações:
// C#
foreach (var c in contexto.Cliente)
contexto.Cliente.Remove(c);
contexto.SaveChanges();
' VB.NET
For Each C In Contexto.Cliente
Contexto.Cliente.Remove(C)
Next
Contexto.SaveChanges()
Porém, ao executarmos o nosso projeto, receberemos o seguinte erro:
Por padrão, o Entity Framework usa o “schema” chamado “dbo“. Ao trabalhar com o Oracle no Entity Framework, descobri nesta thread do StackOverflow que temos que alterar o schema padrão no contexto de forma que ele fique com o mesmo nome do usuário (no meu caso, “DBUSER“). Para isso, temos que fazer um override no método “OnModelCreating” do nosso contexto e, dentro desse método, temos que fazer uma chamada ao método “HasDefaultSchema“, passando o nome do usuário:
// C#
public class EfContext : System.Data.Entity.DbContext
{
public System.Data.Entity.DbSet<Cliente> Cliente { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema("DBUSER");
}
}
' VB.NET
Public Class EfContext
Inherits System.Data.Entity.DbContext
Public Property Cliente As System.Data.Entity.DbSet(Of Cliente)
Protected Overrides Sub OnModelCreating(modelBuilder As Entity.DbModelBuilder)
MyBase.OnModelCreating(modelBuilder)
modelBuilder.HasDefaultSchema("DBUSER")
End Sub
End Class
Será que depois desse ajuste a gente consegue rodar o projeto sem erros? Infelizmente não. Vamos analisar o próximo erro:
Hmm. O nome já está sendo usado? Que estranho. Vamos dar uma olhada no banco de dados novamente, dando um “refresh” nas tabelas para ver o que o Entity Framework aprontou:
Veja só. Além da nossa tabela “CLIENTE“, o Entity Framework tentou criar a tabela “Clientes” (note o “s” no final) e provavelmente deu algum conflito com a tabela “CLIENTE” quanto ao nome da trigger que faz o auto-incremento do “Id“. Isso acontece porque a pluralização de nomes de tabelas é habilitada por padrão no Entity Framework. Para desabilitá-la, temos que fazer um ajuste no nosso método “OnModelCreating“, removendo a convenção de pluralização do nosso contexto:
// C#
public class EfContext : System.Data.Entity.DbContext
{
public System.Data.Entity.DbSet<Cliente> Cliente { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema("DBUSER");
modelBuilder.Conventions.Remove<System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention>();
}
}
' VB.NET
Public Class EfContext
Inherits System.Data.Entity.DbContext
Public Property Cliente As System.Data.Entity.DbSet(Of Cliente)
Protected Overrides Sub OnModelCreating(modelBuilder As Entity.DbModelBuilder)
MyBase.OnModelCreating(modelBuilder)
modelBuilder.HasDefaultSchema("DBUSER")
modelBuilder.Conventions.Remove(Of System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention)()
End Sub
End Class
Agora vamos deletar a tabela “Clientes” que o Entity Framework criou erroneamente no nosso banco de dados e vamos executar a nossa aplicação novamente. Ao fazermos isso, receberemos o mesmo erro (dizendo que o nome já está sendo utilizado). O que será que aconteceu agora? Após um “refresh” nas tabelas, temos a seguinte situação:
Mas o que é isso? O Entity Framework não viu que a tabela “CLIENTE” já existia e acabou tentando criar uma outra tabela chamada “Cliente” (sem ser todas as letras maiúsculas)? Pois é. O Entity Framework trabalha com aspas simples ao redor de nomes de tabelas e campos. Isso faz com que o Oracle funcione em modo sensível a letras maiúsculas e minúsculas! Ou seja, para consertar isso, ou nós descartamos a tabela “CLIENTE” e passamos a trabalhar com a nova tabela “Cliente“, ou nós temos que fazer o mapeamento do nome da tabela e propriedades no método “OnModelCreating“:
' VB.NET
Public Class EfContext
Inherits System.Data.Entity.DbContext
Public Property Cliente As System.Data.Entity.DbSet(Of Cliente)
Protected Overrides Sub OnModelCreating(modelBuilder As Entity.DbModelBuilder)
MyBase.OnModelCreating(modelBuilder)
modelBuilder.HasDefaultSchema("DBUSER")
modelBuilder.Conventions.Remove(Of System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention)()
modelBuilder.Entity(Of Cliente)() _
.Property(Function(C) C.Id) _
.HasColumnName("ID")
modelBuilder.Entity(Of Cliente)() _
.Property(Function(C) C.Nome) _
.HasColumnName("NOME")
modelBuilder.Entity(Of Cliente)().ToTable("CLIENTE")
End Sub
End Class
Pronto! Agora sim o nosso código funcionará corretamente, considerando a tabela “CLIENTE“:
Veja só como fica o código completo para fazer todas as operações CRUD com o Entity Framework:
// C#
using (var contexto = new EfContext())
{
// DELETE
foreach (var c in contexto.Cliente)
contexto.Cliente.Remove(c);
contexto.SaveChanges();
// INSERT
contexto.Cliente.Add(new Cliente() { Nome = "Novo Cliente EF" });
contexto.SaveChanges();
// UPDATE
var cliente = contexto.Cliente.First();
cliente.Nome = "Novo Cliente EF Alterado";
contexto.SaveChanges();
// SELECT
foreach (var c in contexto.Cliente)
{
Console.WriteLine("Nome do Cliente: {0}", c.Nome);
}
}
' VB.NET
Using Contexto = New EfContext()
' DELETE
For Each C In Contexto.Cliente
Contexto.Cliente.Remove(C)
Next
Contexto.SaveChanges()
' INSERT
Contexto.Cliente.Add(New Cliente() With {.Nome = "Novo Cliente EF"})
Contexto.SaveChanges()
' UPDATE
Dim Cliente = Contexto.Cliente.First()
Cliente.Nome = "Novo Cliente EF Alterado"
Contexto.SaveChanges()
' SELECT
For Each C In Contexto.Cliente
Console.WriteLine("Nome do Cliente: {0}", C.Nome)
Next
End Using
Concluindo
Muitas vezes nós ficamos mal-acostumados quando trabalhamos com .NET e SQL Server. Nesse tipo de ambiente tudo normalmente funciona perfeitamente de primeira. Quando partimos para alguma outra tecnologia que não seja da Microsoft, aí começam a surgir os problemas. E foi isso que experimentamos na pele ao tentarmos utilizar o Oracle com C# e VB.NET no artigo dessa semana.
Não há dúvidas que o Oracle é um banco de dados importantíssimo, afinal de contas, como eu mencionei no começo do artigo, ele é atualmente o banco de dados relacional mais utilizado no mundo. Entretanto, podemos concordar que a sua configuração e utilização através do .NET não é lá tão simples assim, principalmente quando adicionamos um ORM como o Entity Framework no meio do caminho (cenário muito comum hoje em dia).
No artigo de hoje, você aprendeu a instalar o Oracle Database Express (versão gratuita do Oracle) e conheceu as suas limitações. Em seguida, eu te apresentei a ferramenta de administração da própria Oracle (o Oracle SQL Developer) que, apesar de não chegar nem perto do SQL Server Management Studio (na minha opinião), até que dá conta do recado. Por fim, você viu como instalar o provider e ferramentas de suporte ao Visual Studio, além de uma implementação bem simples de CRUD com ADO.NET puro e Entity Framework.
E aí, qual a sua opinião? Você acha que o SQL Server é mais simples de instalar e configurar? Já teve que trabalhar com o Oracle nos seus projetos? Passou pelos mesmos problemas que eu passei? Conte-nos mais detalhes na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Um dos artigos mais acessados até hoje aqui no site é aquele onde eu mostrei como podemos fazer backup e restauração do SQL Server através do C#. Esses dias atrás, recebi um e-mail de alguém que tinha lido esse artigo perguntando como ele poderia verificar se uma instância do SQL Server já estava instalada e rodando (por exemplo, uma instância chamada “SQLEXPRESS“). Pensei comigo: “Hmm, que dúvida interessante, ainda não sei como fazer isso“. Resolvi pesquisar e acabei descobrindo que conseguimos listar facilmente as instâncias do SQL Server com C# e VB.NET através do SMO (que é a mesma biblioteca que utilizamos para fazer o backup e restauração). Vamos ver como podemos implementar essa funcionalidade na nossa aplicação?
Criando o projeto de exemplo
Para entendermos como é que funciona o processo de listagem das instâncias do SQL Server, vamos criar um novo projeto do tipo “Windows Forms Application“. Não se engane: o código que eu vou mostrar nesse artigo funciona nas outras plataformas também (como WPF, ASP.NET, Console Application, etc). Só escolhi Windows Forms para facilitar a nossa vida.
Uma vez criado o projeto, vamos adicionar as referências às bibliotecas do SMO (SQL Server Management Objects). Você encontra esses itens dentro da categoria “Extensions” da tela “Reference Manager“:
Em seguida, vamos ajustar o formulário do nosso projeto, adicionando um DataGridView (dataGridViewInstancias) e um botão (listarSQLButton), de forma que ele fique parecido com a imagem a seguir:
Listando todas as instâncias (rede e local)
Agora que já temos o formulário criado e as referências do SMO adicionadas ao projeto, vamos implementar o código para listarmos todas as instâncias que conseguirmos encontrar, tanto na rede quanto no computador local. Isso é muito tranquilo de ser feito com o SMO, mais especificamente com a classe SmoApplication. Essa classe possui um método estático (chamado EnumAvailableSqlServers) que implementa justamente a funcionalidade de listar as instâncias do SQL Server.
O método “EnumAvailableSqlServers” possui três sobrecargas. A primeira delas não recebe nenhum parâmetro, e o resultado da sua chamada será uma DataTable contendo as informações de todas as instâncias que forem encontradas, tanto na rede quanto no computador onde a aplicação está sendo executada. O código do evento de clique no nosso botão deve ficar assim:
' VB.NET
Private Sub listarSQLButton_Click(sender As Object, e As EventArgs) Handles listarSQLButton.Click
dataGridViewInstancias.DataSource = Microsoft.SqlServer.Management.Smo.SmoApplication.EnumAvailableSqlServers()
End Sub
Execute a aplicação, clique no botão e veja o resultado:
Filtrando a pesquisa pelo nome da instância
Em algumas situações, pode ser que precisemos procurar por um nome de instância específico. Nesse caso, ao invés de simplesmente chamarmos o método “EnumAvailableSqlServers” e filtrarmos o resultado, podemos utilizar a sobrecarga do método que recebe uma string, que é justamente o filtro que queremos utilizar na busca.
Vamos, então, adicionar um TextBox no formulário (nomeInstanciaTextBox), onde o usuário poderá especificar o nome do servidor e instância que ele quer procurar:
Em seguida, vamos fazer uma pequena alteração no código para considerarmos esse TextBox na consulta:
' VB.NET
Private Sub listarSQLButton_Click(sender As Object, e As EventArgs) Handles listarSQLButton.Click
Dim servers As DataTable = Nothing
If Not String.IsNullOrWhiteSpace(nomeInstanciaTextBox.Text) Then
servers = Microsoft.SqlServer.Management.Smo.SmoApplication.EnumAvailableSqlServers(nomeInstanciaTextBox.Text)
Else
servers = Microsoft.SqlServer.Management.Smo.SmoApplication.EnumAvailableSqlServers()
End If
dataGridViewInstancias.DataSource = servers
End Sub
Pronto! Agora podemos procurar por uma instância específica do SQL Server:
Pesquisando somente instâncias locais
Uma outra opção que podemos adicionar no nosso formulário é alternativa de pesquisarmos somente as instâncias locais. A terceira sobrecarga do método “EnumAvailableSqlServers” recebe um parâmetro booleano. Quando passamos “true” para esse parâmetro, o método fará a pesquisa somente no computador onde a aplicação está rodando (e não na rede toda).
Para implementarmos essa opção, vamos adicionar um CheckBox no nosso formulário (somenteInstanciasLocaisCheckBox):
E, no código, utilizamos a informação da propriedade “Checked” no parâmetro do método “EnumAvailableSqlServers“:
' VB.NET
Private Sub listarSQLButton_Click(sender As Object, e As EventArgs) Handles listarSQLButton.Click
Dim servers As DataTable = Nothing
If Not String.IsNullOrWhiteSpace(nomeInstanciaTextBox.Text) Then
servers = Microsoft.SqlServer.Management.Smo.SmoApplication.EnumAvailableSqlServers(nomeInstanciaTextBox.Text)
Else
servers = Microsoft.SqlServer.Management.Smo.SmoApplication.EnumAvailableSqlServers(somenteInstanciasLocaisCheckBox.Checked)
End If
dataGridViewInstancias.DataSource = servers
End Sub
Feito isso, ao pesquisarmos as instâncias com o CheckBox marcado, somente as instâncias locais serão retornadas (no meu caso, não tenho nenhuma instância local instalada, por isso a lista está vazia):
Concluindo
As classes disponíveis no pacote SQL Server Management Object possuem funcionalidades muito interessantes para gerenciarmos bancos de dados SQL Server. Uns tempos atrás eu mostrei como criar e restaurar backups do SQL Server utilizando essas classes. Hoje eu resolvi mostrar como podemos utilizar essas mesmas classes para pesquisarmos instâncias do SQL Server com C# e VB.NET.
Você já teve a necessidade de saber se uma instância do SQL Server estava disponível ou não? Se sim, como é que você acabou resolvendo essa necessidade? Utilizando as classes do SMO, como eu demonstrei nesse artigo, ou de alguma outra forma? Conte-nos mais detalhes na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Esses dias atrás eu recebi um comentário no meu artigo sobre impressão direta com C# perguntando se seria possível imprimirmos um formulário no Windows Forms. Eu sabia que isso era possível, então, dei uma pesquisada rápida e encaminhei este link para ele. Porém, como eu já tinha recebido esse tipo de pergunta mais de uma vez (além das diversas vezes que já me deparei com essa dúvida nos fóruns da MSDN), percebi que essa era uma demanda recorrente. Então, eu pensei: que tal escrever um artigo sobre essa funcionalidade? Quer conferir o resultado? Então, vamos lá!
Capturando uma imagem do formulário
Imprimir o formulário no Windows Forms não é uma tarefa nada difícil, principalmente se pudermos considerar a borda na do formulário. Um simples objeto do tipo Graphics em conjunto com a chamada do seu método CopyFromScreen atende facilmente essa demanda. Esse método faz a captura de uma determinada área da tela. Dessa forma, se nós simplesmente passarmos as coordenadas do formulário, teremos o resultado esperado. Vamos ver como podemos fazer isso?
Primeiramente, vamos criar um novo projeto do tipo “Windows Forms Application“. Dentro do formulário, vamos adicionar um botão (“imprimirButton“) e um componente do tipo PrintDocument (“printDocument“):
Em seguida, no evento “Click” do botão, vamos chamar o método “Print” do nosso PrintDocument:
' VB.NET
Private Sub ImprimirButton_Click(sender As Object, e As EventArgs) Handles ImprimirButton.Click
PrintDocument.Print()
End Sub
A definição do que será impresso pelo componente PrintDocument é feita através do seu evento “PrintPage“. Para implementarmos o código nesse evento, vamos até o designer do formulário, clicamos no componente PrintDocument e, na janela de propriedades, nós temos que encontrar o evento “PrintPage” e, em seguida, clicamos duas vezes para criarmos um novo manipulador para esse evento:
Dentro desse evento, vamos capturar o desktop, limitando para a área onde o formulário está localizado:
// C#
private void printDocument_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
{
var image = new Bitmap(this.Width, this.Height);
var graphics = Graphics.FromImage(image);
graphics.CopyFromScreen(this.Location.X, this.Location.Y, 0, 0, this.Size);
e.Graphics.DrawImage(image, 20, 20);
}
' VB.NET
Private Sub PrintDocument_PrintPage(sender As Object, e As Printing.PrintPageEventArgs) Handles PrintDocument.PrintPage
Dim Image = New Bitmap(Me.Width, Me.Height)
Dim graphics = System.Drawing.Graphics.FromImage(Image)
graphics.CopyFromScreen(Me.Location.X, Me.Location.Y, 0, 0, Me.Size)
e.Graphics.DrawImage(Image, 20, 20)
End Sub
Nota: a imagem do formulário será impressa na coordenada 20,20 da página. Se você quiser imprimir o formulário em uma outra coordenada, basta alterar os dois últimos valores dos parâmetros do método “DrawImage”.
Agora execute a aplicação e clique no botão “Imprimir“. A impressão será feita na impressora “padrão“. Para alterar a impressora, você pode configurar o nome da impressora na propriedade “PrinterSettings.PrinterName” (do componente PrintDocument) ou implementar uma funcionalidade onde o usuário possa escolher qual impressora utilizar, como eu mostrei no meu artigo sobre impressão direta na impressora. O resultado da impressão será parecido com a imagem abaixo:
Notou o problema? O método “CopyFromScreen” realmente copia tudo o que está sendo exibido na tela no momento da sua chamada, inclusive diálogos que possam estar por cima do formulário (como foi o caso do diálogo de impressão que foi capturado no nosso exemplo). Para que o diálogo não seja capturado na impressão, temos que chamar o método “CopyFromScreen” antes de chamarmos o método “Print” do “PrintDialog“, armazenando o resultado em uma variável no nível do formulário, que posteriormente será utilizada dentro do evento “PrintPage“.
Dito isso, a primeira coisa que temos que fazer é extrairmos a captura em um método (que daremos o nome de “CapturarForm“):
' VB.NET
Private Captura As Bitmap
Private Sub CapturarForm()
Captura = New Bitmap(Me.Width, Me.Height)
Dim graphics = System.Drawing.Graphics.FromImage(Captura)
graphics.CopyFromScreen(Me.Location.X, Me.Location.Y, 0, 0, Me.Size)
End Sub
Em seguida, vamos ajustar o código que faz a impressão, de forma que esse novo método seja chamado antes do “Print” do “PrintDocument” e que o atributo “Captura” seja utilizado dentro do evento “PrintPage“:
' VB.NET
Private Sub ImprimirButton_Click(sender As Object, e As EventArgs) Handles ImprimirButton.Click
CapturarForm()
PrintDocument.Print()
End Sub
Private Sub PrintDocument_PrintPage(sender As Object, e As Printing.PrintPageEventArgs) Handles PrintDocument.PrintPage
e.Graphics.DrawImage(Captura, 20, 20)
End Sub
Pronto! Veja só o resultado na imagem a seguir:
Como você pode perceber, o diálogo de impressão não é mais capturado na imagem. Porém, você viu que ainda temos um outro problema para resolver? No Windows 10, temos esse efeito de transparência ao redor da janela, que faz com que o conteúdo que estiver embaixo do formulário seja impresso também.
Desconsiderando o efeito de transparência do Windows 10
Depois de procurar bastante, acabei encontrando duasthreads no StackOverflow mostrando como pegar as coordenadas do formulário desconsiderando o efeito de transparência da borda. A solução consiste em chamar um método da dll “dwmapi“, que retornará as coordenadas verdadeiras da janela.
Para consertarmos esse problema, vamos criar uma nova classe no nosso projeto, a qual daremos o nome de “WindowHelper“. O conteúdo dessa classe deverá ser o seguinte:
// C#
public static class WindowHelper
{
[Serializable, System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public System.Drawing.Rectangle ToRectangle()
{
return System.Drawing.Rectangle.FromLTRB(Left, Top, Right, Bottom);
}
}
[System.Runtime.InteropServices.DllImport(@"dwmapi.dll")]
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out Rect pvAttribute, int cbAttribute);
public enum Dwmwindowattribute
{
DwmwaExtendedFrameBounds = 9
}
}
' VB.NET
Public Module WindowHelper
<Serializable, System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)>
Public Structure Rect
Public Left As Integer
Public Top As Integer
Public Right As Integer
Public Bottom As Integer
Public Function ToRectangle() As System.Drawing.Rectangle
Return System.Drawing.Rectangle.FromLTRB(Left, Top, Right, Bottom)
End Function
End Structure
<System.Runtime.InteropServices.DllImport("dwmapi.dll")>
Public Function DwmGetWindowAttribute(hwnd As IntPtr, dwAttribute As Integer, ByRef pvAttribute As Rect, cbAttribute As Integer) As Integer
End Function
Public Enum Dwmwindowattribute
DwmwaExtendedFrameBounds = 9
End Enum
End Module
Em seguida, temos que ajustar o nosso método que faz a captura do formulário, de forma que ele utilize os valores retornados por essa nova classe que acabamos de criar:
// C#
private void CapturarForm()
{
WindowHelper.Rect rect;
WindowHelper.DwmGetWindowAttribute(this.Handle, (int)WindowHelper.Dwmwindowattribute.DwmwaExtendedFrameBounds,
out rect, System.Runtime.InteropServices.Marshal.SizeOf(typeof(WindowHelper.Rect)));
var rectangle = rect.ToRectangle();
captura = new Bitmap(rectangle.Width, rectangle.Height);
var graphics = Graphics.FromImage(captura);
graphics.CopyFromScreen(rectangle.Left, rectangle.Top, 0, 0, rectangle.Size);
}
' VB.NET
Private Sub CapturarForm()
Dim Rect As WindowHelper.Rect
WindowHelper.DwmGetWindowAttribute(Me.Handle, CInt(WindowHelper.Dwmwindowattribute.DwmwaExtendedFrameBounds),
Rect, System.Runtime.InteropServices.Marshal.SizeOf(GetType(WindowHelper.Rect)))
Dim Rectangle = Rect.ToRectangle()
Captura = New Bitmap(Rectangle.Width, Rectangle.Height)
Dim Graphics = System.Drawing.Graphics.FromImage(Captura)
Graphics.CopyFromScreen(Rectangle.Left, Rectangle.Top, 0, 0, Rectangle.Size)
End Sub
Execute a aplicação, clique no botão “Imprimir” e veja o novo resultado:
Agora sim, hein?
Imprimindo o formulário sem a borda
Em algumas situações, pode ser que precisemos imprimir somente o conteúdo interno do formulário, ou seja, desconsiderando totalmente as bordas. Pesquisei um pouco sobre esse tema, encontrei algumas possibilidades para calcularmos as coordenadas da área interna do formulário, mas, todas as opções me pareceram muito “gambiarra“.
Uma alternativa bem simples nesse caso seria, antes de realizarmos a captura, nós alterarmos o estilo da janela, removendo as bordas através da propriedade FormBorderStyle. Depois da captura, nós restauramos o estilo de borda anterior. Eu sei que essa solução não é das mais bonitas, mas, eu acho que às vezes a solução mais “feia” acaba sendo a mais recomendada. Imagina se nós utilizamos um método de calcular as coordenadas internas do formulário e na próxima versão do Windows esse método para de funcionar? Melhor evitar essa situação, não é mesmo?
Dito isso, para capturarmos o formulário sem as bordas, nós poderíamos fazer o seguinte ajuste no método “CapturarForm“:
// C#
private void CapturarForm()
{
var formBorderStyleAnterior = this.FormBorderStyle;
try
{
this.FormBorderStyle = FormBorderStyle.None;
WindowHelper.Rect rect;
WindowHelper.DwmGetWindowAttribute(this.Handle, (int)WindowHelper.Dwmwindowattribute.DwmwaExtendedFrameBounds,
out rect, System.Runtime.InteropServices.Marshal.SizeOf(typeof(WindowHelper.Rect)));
var rectangle = rect.ToRectangle();
captura = new Bitmap(rectangle.Width, rectangle.Height);
var graphics = Graphics.FromImage(captura);
graphics.CopyFromScreen(rectangle.Left, rectangle.Top, 0, 0, rectangle.Size);
}
finally
{
this.FormBorderStyle = formBorderStyleAnterior;
}
}
' VB.NET
Private Sub CapturarForm()
Dim FormBorderStyleAnterior = Me.FormBorderStyle
Try
Me.FormBorderStyle = FormBorderStyle.None
Dim Rect As WindowHelper.Rect
WindowHelper.DwmGetWindowAttribute(Me.Handle, CInt(WindowHelper.Dwmwindowattribute.DwmwaExtendedFrameBounds),
Rect, System.Runtime.InteropServices.Marshal.SizeOf(GetType(WindowHelper.Rect)))
Dim Rectangle = Rect.ToRectangle()
Captura = New Bitmap(Rectangle.Width, Rectangle.Height)
Dim Graphics = System.Drawing.Graphics.FromImage(Captura)
Graphics.CopyFromScreen(Rectangle.Left, Rectangle.Top, 0, 0, Rectangle.Size)
Finally
Me.FormBorderStyle = FormBorderStyleAnterior
End Try
End Sub
Nota: o bloco try-finally assegura que o FormBorderStyle anterior seja sempre restaurado, independentemente se uma exceção tiver sido disparada durante o processo de impressão.
Veja só o resultado da impressão após essas alterações:
Imprimindo uma área específica do formulário
Outra demanda que pode surgir é a necessidade de imprimirmos somente uma área específica do formulário, e não o formulário inteiro. Isso é muito fácil de ser resolvido. Basta ajustarmos os valores de tamanho e coordenadas utilizados no método “CapturarForm“.
Por exemplo, se quisermos imprimir uma área de 200 x 200 pixels do nosso formulário, começando na coordenada 15, 15, o código do método “CapturarForm” ficaria assim:
// C#
private void CapturarForm()
{
captura = new Bitmap(200, 200);
var graphics = Graphics.FromImage(captura);
graphics.CopyFromScreen(this.Location.X + 15, this.Location.Y + 15, 0, 0, new Size(200, 200));
}
' VB.NET
Private Sub CapturarForm()
Captura = New Bitmap(200, 200)
Dim Graphics = System.Drawing.Graphics.FromImage(Captura)
Graphics.CopyFromScreen(Me.Location.X + 15, Me.Location.Y + 15, 0, 0, New Size(200, 200))
End Sub
E este seria o resultado:
Concluindo
Apesar de não ser tão complicado imprimir um formulário no Windows Forms, existem alguns detalhezinhos que podem acabar nos deixando com o cabelo em pé. Principalmente o efeito de transparência nas bordas dos formulários do Windows 10, que pode se tornar um pesadelo nesse processo.
No artigo de hoje você conferiu como imprimir um formulário no Windows Forms e como lidar com o problema do efeito na borda do Windows 10. Além disso, vimos também como imprimir somente o conteúdo interno do formulário (desconsiderando as bordas) e como imprimir somente uma área específica do formulário.
E você? Já precisou imprimir um formulário no Windows Forms? Também passou por esses problemas que eu mencionei nesse artigo? Como é que você conseguiu resolver essa demanda? Fico aguardando os seus comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Imagine a seguinte situação: você está de boa implementando uma nova funcionalidade no seu projeto quando, do nada, o telefone toca. Um dos clientes do seu sistema está reclamando de um bug que você já corrigiu faz um tempão. E agora? Será que o bug está de volta? Ou será que o cliente está com uma versão antiga da aplicação, na qual o bug ainda não tinha sido corrigido?
É por essas e outras que é importantíssimo controlarmos o número da versão dos nossos assemblies. Dessa forma, conseguimos saber se o usuário já está utilizando a última versão da aplicação. Porém, se você já tentou controlar o número da versão manualmente, você sabe que essa é uma tarefa um tanto quanto chata. Não é à toa que o Visual Studio tem uma funcionalidade de cálculo automático do número de build e revisão. Entretanto, essa funcionalidade do Visual Studio não é nada flexível, uma vez que não conseguimos incrementar o número da versão começando em zero (ele usa uma data e hora fixas como base).
Mas, não se preocupe. Como de costume, existe uma extensão do Visual Studio que implementa essa possibilidade para os nossos projetos. Quer conferir tudo sobre às versões de assemblies no mundo .NET? Então, vem comigo!
O arquivo AssemblyInfo
Sempre que criamos um novo projeto no Visual Studio, ele cria automaticamente um arquivo chamado AssemblyInfo.cs (ou AssemblyInfo.vb no caso de projetos VB.NET). Em projetos C#, conseguimos abrir esse arquivo facilmente pelo Solution Explorer, expandindo o nó “Properties“:
Como falei no parágrafo anterior, no VB.NET nós temos também o arquivo AssemblyInfo. Porém, nós não conseguimos acessá-lo através do Solution Explorer. Para abrirmos o arquivo AssemblyInfo.vb, temos que navegar até o subdiretório “My Project” do nosso projeto através do Windows Explorer:
Já é de se imaginar que esse arquivo armazena diversas informações sobre o assembly – afinal, ele se chama “AssemblyInfo“. Uma das informações que encontramos nesse arquivo é a versão do assembly. Mas, não pense que nós só temos um número de versão para cada assembly. Para complicar as coisas, nós temos a possibilidade de definir 3 (!!) números de versões diferentes.
Primeiramente, temos a versão do assembly propriamente dita, definida com o atributo “AssemblyVersion“:
Em seguida, temos o número da versão do “arquivo do assembly“, que pode ser definida com o atributo “AssemblyFileVersion“:
Por fim, temos um outro número de versão que pouca gente conhece: o número da versão do produto! Essa versão pode ser definida com o atributo “AssemblyInformationalVersion“:
As versões do assembly e do “arquivo do assembly” também podem ser definidas nas propriedades do projeto, clicando no botão “Assembly Information“:
A versão mais importante é a versão do assembly. Se a versão do “arquivo do assembly” estiver em branco, o número da versão do assembly será considerado no seu lugar. O mesmo acontece com a versão do produto – se ela estiver em branco, a versão do “arquivo do assembly” será utilizada. Ou seja, a hierarquia dos números de versões é: versão do assembly => versão do arquivo do assembly => versão do produto.
Além disso, como a descrição do arquivo AssemblyInfo já explica, os números de versão são compostos por quatro seções:
– Primeira seção = versão principal – Segunda seção = versão secundária – Terceira seção = número da build – Quarta seção = número da revisão
Descobrindo o número da versão em tempo de execução
Não é nada difícil descobrirmos o número da versão em tempo de execução. A versão do assembly pode ser acessada através da propriedade Version do assembly. Já as outras versões podem ser acessadas através da classe FileVersionInfo. Por exemplo, em um projeto do tipo “Console Application“, o código para pegarmos os três tipos de versão e imprimirmos no console ficaria assim:
' VB.NET
Dim AssemblyVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()
Dim FileVersion = System.Diagnostics.FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).FileVersion
Dim ProductVersion = System.Diagnostics.FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).ProductVersion
Console.WriteLine(String.Format("Assembly Version: {0}", AssemblyVersion))
Console.WriteLine(String.Format("File Version: {0}", FileVersion))
Console.WriteLine(String.Format("Product Version: {0}", ProductVersion))
Por padrão, a versão do assembly vem configurada como “1.0.0.0” quando criamos um projeto novo no Visual Studio. Dessa forma, o resultado da execução desse código para um projeto feito “do zero” deve ser:
Atenção! Se o seu projeto tiver mais de um assembly (por exemplo, um projeto com um “exe” e várias “dlls“), esse código retornará a versão do assembly onde o código está sendo executado. Ou seja, se você colocar esse código em um assembly que não seja o “exe“, pode ser que o número retornado não seja o que você está esperando. Se você quiser recuperar explicitamente a versão do executável principal da aplicação, substitua a chamada de “GetExecutingAssembly” por “GetEntryAssembly“.
Incrementando o número da build e revisão utilizando asterisco (*)
O próprio Visual Studio possui um mecanismo de atualização automática do número de versão a cada build. Porém, como veremos a seguir, ele não é nada flexível.
A opção que temos no Visual Studio possibilita que incrementemos automaticamente o número da build ou o número da revisão (ou ambos). Para fazer isso, utilizamos um asterisco (*) na terceira ou quarta seção do número da versão. Por exemplo, se utilizarmos o asterisco na última seção, este será o resultado:
Já se utilizarmos o asterisco no número da build, este será o resultado:
Se utilizamos o asterisco no número da build, nós não podemos colocar um número fixo no número da revisão. Ou seja, ao escolhermos fazer o incremento automático do número da build, o número da revisão também terá que ser obrigatoriamente incrementado de forma automática.
Porém, você viu que o Visual Studio utilizou uns valores bastante esquisitos para o número da build e revisão? Pois é, os números não serão incrementados de um em um, mas sim, utilizando uma lógica pré-estabelecida.
Ao utilizarmos asterisco no número da build, ela será incrementada diariamente. O valor atribuído será a diferença de dias entre hoje e o dia 1 de janeiro de 2000. Você pode confirmar isso fazendo a conta no Excel:
Para o número da revisão, o Visual Studio considerará a quantidade de segundos desde a meia noite do dia atual. Ou seja, para cada compilação, o assembly receberá um número de revisão diferente.
Esse sistema de numeração automática do Visual Studio até que é legalzinho e muito fácil de utilizar, mas, como falei anteriormente, ele não é nada flexível. Se quisermos criar uma metodologia de incremento que não considere a data/hora, mas sim, um incremento começando com o número “1“, nós teríamos que fazer o controle manualmente.
Extensão Automatic Versions
Para nossa sorte, o Visual Studio é extremamente extensível. Não sei se você já deu uma olhada no Visual Studio Gallery, mas, tem extensão para tudo quanto é coisa! Uma extensão muito útil no que diz respeito ao incremento de versões é a Automatic Versions.
Ao instalarmos essa extensão, uma nova opção será adicionada no menu “Tools” do Visual Studio:
Ao clicarmos nesse item, veja só que legal:
Note que podemos configurar números de versões globalmente, para a solução ou para projetos específicos dentro da solução. Por exemplo, para configurarmos o nosso projeto de forma que o número da revisão seja incrementado em cada compilação (começando em “1“), fazemos o seguinte:
Feito isso, temos que voltar nas propriedades do projeto para removermos o asterisco que colocamos anteriormente no número da build:
Em seguida, se recompilarmos o nosso projeto, o número da revisão será incrementado automaticamente:
Para cada vez que recompilarmos o projeto, o número da revisão será incrementado em “1“. Legal, não? Pois saiba que a extensão Automatic Versions disponibiliza diversas outras opções para numerarmos os nossos assemblies:
Por exemplo, se quisermos que o número da versão tome como base a data e hora da compilação, basta configurá-lo da seguinte maneira:
E esse seria o resultado:
Concluindo
Uma vez que começamos a distribuir os nossos aplicativos para os nossos clientes, é importante fazermos o controle do número da versão. Dessa forma, fica fácil saber qual versão o usuário está executando, sem termos que ficar olhando a data dos arquivos.
O controle do número da versão dos nossos assemblies pode obviamente ser feito de maneira manual, porém, isso não é nada prático. Por isso, o Visual Studio traz consigo a possibilidade de incrementarmos automaticamente o número da build e o número da revisão para cada compilação. Entretanto, essa funcionalidade do Visual Studio tem como base uma data e hora fixas, fazendo com que a versão dos nossos assemblies fique um tanto quanto esquisitas.
Para fazermos com que o número da versão seja incrementado em cada processo de compilação, podemos utilizar a extensão chamada Automatic Versions. Essa extensão possibilita definirmos incrementos automáticos da versão ou até mesmo lógicas mais complexas (como a utilização da data/hora da compilação como versão do assembly).
No artigo de hoje você conferiu todas essas opções de numeração de versões dos nossos assemblies com o Visual Studio. E você? Como você faz a numeração dos assemblies do seu projeto? Manualmente? Utilizando o incremento do Visual Studio? Ou quem sabe utilizando uma extensão para fazer isso de forma automática? Compartilhe os detalhes das suas estratégias de numeração de versão nos comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Quem trabalha com o Report Viewer provavelmente já sabe que ele não permite a exibição de dados vindos de múltiplas tabelas em um único controle. Isso quer dizer que, se você quiser exibir dados de mais de uma tabela em um controle Tablix, você terá que partir para alguma alternativa.
A alternativa mais utilizada é a consolidação dos dados em uma única DataTable (ou classe) que servirá como fonte de dados para os nossos relatórios. Porém, essa não é a melhor das alternativas, uma vez que teremos que potencialmente construir DataSets e DataTables customizados para cada relatório do nosso sistema.
O que pouca gente conhece é que existe a expressão “Lookup” no Report Viewer, que nos permite justamente pegar informações de outras tabelas. No artigo de hoje eu vou mostrar para você como utilizar essa expressão. Você quer deixar os seus relatórios do Report Viewer mais simples? Então continue lendo e veja como a expressão “Lookup” pode simplificar consideravelmente os seus relatórios.
Entendendo o problema
Antes de apresentar a solução, vamos entender o problema? Para isso, vamos criar um exemplo onde temos que desenvolver um relatório que exibirá dados vindos de múltiplas tabelas. Temos inúmeros cenários onde isso se faz necessário, mas, para não complicarmos muito, preparei um exemplo bem simples para que vocês possam entender mais facilmente.
Imagine que a nossa tarefa seja gerar um relatório sobre uma entidade chamada “Pessoa” em um sistema de Recursos Humanos. Essa entidade, além das propriedades habituais, tem também uma ligação com a entidade “Empresa” (a pessoa trabalha em algum lugar) e outra ligação com a entidade “Cargo” (ela ocupa uma determinada posição nessa empresa).
Para simularmos essa estrutura, vamos começar criando um novo projeto do tipo “Windows Forms Application“. Dentro desse projeto, adicione um novo DataSet tipado (dando o nome de “DataSetPessoa“) com as seguintes tabelas, colunas e relacionamentos:
Aproveitando que temos um DataSet tipado, vamos dar um duplo clique para implementarmos um método que alimentará esse DataSet com alguns dados de exemplo. Obviamente, em um sistema “de verdade” você teria que ler esses dados do banco:
// C#
partial class DataSetPessoa
{
public void PreencherComDadosDeExemplo()
{
var abcdev = this.Empresa.AddEmpresaRow("abcdev", "AA Lima Dev EPP", "99.999.999/9999-99");
var microsoft = this.Empresa.AddEmpresaRow("Microsoft", "Microsoft BR Ltda", "99.999.999/9999-99");
var google = this.Empresa.AddEmpresaRow("Google", "Google Brasil S/A", "99.999.999/9999-99");
var analista = this.Cargo.AddCargoRow("Analista de Sistemas", "XXX");
var arquiteto = this.Cargo.AddCargoRow("Arquiteto de Sistemas", "YYY");
var diretor = this.Cargo.AddCargoRow("Diretor Administrativo", "ZZZ");
this.Pessoa.AddPessoaRow("Andre", "Lima", new System.DateTime(1984, 1, 1), abcdev, analista, "999.999.99-99");
this.Pessoa.AddPessoaRow("Larissa", "Lima", new System.DateTime(1987, 2, 2), abcdev, diretor, "999.999.99-99");
this.Pessoa.AddPessoaRow("Fulano", "de Tal", new System.DateTime(1978, 3, 3), microsoft, arquiteto, "999.999.99-99");
this.Pessoa.AddPessoaRow("Paula", "Bellizia", new System.DateTime(1970, 4, 4), microsoft, diretor, "999.999.99-99");
this.Pessoa.AddPessoaRow("Fabio", "Coelho", new System.DateTime(1968, 5, 5), google, diretor, "999.999.99-99");
}
}
' VB.NET
Partial Class DataSetPessoa
Public Sub PreencherComDadosDeExemplo()
Dim Abcdev = Me.Empresa.AddEmpresaRow("abcdev", "AA Lima Dev EPP", "99.999.999/9999-99")
Dim Microsoft = Me.Empresa.AddEmpresaRow("Microsoft", "Microsoft BR Ltda", "99.999.999/9999-99")
Dim Google = Me.Empresa.AddEmpresaRow("Google", "Google Brasil S/A", "99.999.999/9999-99")
Dim Analista = Me.Cargo.AddCargoRow("Analista de Sistemas", "XXX")
Dim Arquiteto = Me.Cargo.AddCargoRow("Arquiteto de Sistemas", "YYY")
Dim Diretor = Me.Cargo.AddCargoRow("Diretor Administrativo", "ZZZ")
Me.Pessoa.AddPessoaRow("Andre", "Lima", New System.DateTime(1984, 1, 1), Abcdev, Analista, "999.999.99-99")
Me.Pessoa.AddPessoaRow("Larissa", "Lima", New System.DateTime(1987, 2, 2), Abcdev, Diretor, "999.999.99-99")
Me.Pessoa.AddPessoaRow("Fulano", "de Tal", New System.DateTime(1978, 3, 3), Microsoft, Arquiteto, "999.999.99-99")
Me.Pessoa.AddPessoaRow("Paula", "Bellizia", New System.DateTime(1970, 4, 4), Microsoft, Diretor, "999.999.99-99")
Me.Pessoa.AddPessoaRow("Fabio", "Coelho", New System.DateTime(1968, 5, 5), Google, Diretor, "999.999.99-99")
End Sub
End Class
E agora? Como poderíamos fazer para montarmos um relatório que lista as pessoas desse DataSet, porém, incluindo as informações da empresa onde ela trabalha e as informações do cargo que ela ocupa?
Primeira alternativa: juntando tudo em uma única tabela
A primeira alternativa para resolvermos essa situação (e a mais utilizada) é criarmos um novo DataSet (ou classe) juntando todas as colunas que queremos exibir no relatório em uma única tabela. Ou seja, no nosso caso, teríamos somente uma tabela com todas as informações da Pessoa, Empresa e Cargo.
Para implementarmos essa alternativa, vamos adicionar um segundo DataSet tipado no nosso projeto (dando o nome de “DataSetRelatorio“), que deverá conter a seguinte tabela:
Vamos também dar um duplo clique no DataSet para criarmos um método que receberá uma instância de “DataSetPessoa” e juntará tudo na tabela unificada:
' VB.NET
Partial Class DataSetRelatorio
Public Sub DataSetRelatorio(DsPessoa As DataSetPessoa)
For Each PessoaRow In DsPessoa.Pessoa
Me.Pessoa.AddPessoaRow(
PessoaRow.PessoaID,
PessoaRow.Nome,
PessoaRow.Sobrenome,
PessoaRow.DataNascimento,
PessoaRow.EmpresaID,
PessoaRow.CargoID,
PessoaRow.CPF,
PessoaRow.EmpresaRow.NomeFantasia,
PessoaRow.EmpresaRow.RazaoSocial,
PessoaRow.EmpresaRow.CNPJ,
PessoaRow.CargoRow.Nome,
PessoaRow.CargoRow.Descricao)
Next
End Sub
End Class
Com esse DataSet em mãos, podemos simplesmente criar um novo relatório baseado nessa DataTable unificada. Vamos dar o nome de “RelatorioPessoa” para esse novo relatório:
Dentro desse relatório, vamos adicionar um componente do tipo “Table“. Quando fazemos isso, o Visual Studio automaticamente nos perguntará as informações sobre o DataSet que deverá alimentar essa tabela. Nessa janela, vamos dar o nome de “DataSetPessoa” para o DataSet que será criado e vamos escolher a tabela “Pessoa” do nosso “DataSetRelatorio“:
Em seguida, vamos arrastar cada uma das colunas do DataSet para dentro da tabela:
O resultado final deverá ficar parecido com a imagem abaixo:
Por fim, vamos até o nosso formulário, onde arrastaremos um controle visualizador do Report Viewer para dentro do formulário e escolheremos o relatório que acabamos de criar:
No code-behind do formulário, vamos criar os DataSets que serão passados para o relatório:
// C#
private void Form1_Load(object sender, EventArgs e)
{
var dsPessoa = new DataSetPessoa();
dsPessoa.PreencherComDadosDeExemplo();
var dsRelatorio = new DataSetRelatorio(dsPessoa);
this.reportViewer1.LocalReport.DataSources.Clear();
this.reportViewer1.LocalReport.DataSources.Add(new Microsoft.Reporting.WinForms.ReportDataSource("DataSetPessoa", (DataTable)dsRelatorio.Pessoa));
this.reportViewer1.RefreshReport();
}
' VB.NET
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim DsPessoa As New DataSetPessoa()
DsPessoa.PreencherComDadosDeExemplo()
Dim DsRelatorio As New DataSetRelatorio(DsPessoa)
Me.ReportViewer1.LocalReport.DataSources.Clear()
Me.ReportViewer1.LocalReport.DataSources.Add(New Microsoft.Reporting.WinForms.ReportDataSource("DataSetPessoa", DirectCast(DsRelatorio.Pessoa, DataTable)))
Me.ReportViewer1.RefreshReport()
End Sub
Pronto! Execute a aplicação e confira o resultado:
Obviamente o relatório poderia ser melhor formatado, mas, como esse não é o foco desse artigo, resolvi deixa-lo com a formatação padrão.
Alternativa mais simples: múltiplas tabelas + expressão Lookup
Como vimos na seção anterior, para exibirmos dados de múltiplas tabelas no Report Viewer, nós podemos criar um novo DataSet (ou classe) juntando todos os dados em uma só estrutura. Apesar de essa alternativa resolver o problema, muitas vezes ela acaba sendo um tanto quanto custosa. Imagine se o nosso sistema tiver diversos relatórios e todos eles tiverem que exibir dados de múltiplas tabelas? Teríamos que criar um DataSet novo para cada relatório, o que seria possível, mas, impraticável.
Uma alternativa mais simples seria trabalharmos com vários DataSets no mesmo relatório, sendo que um DataSet apontaria para a tabela “principal” (no nosso caso a tabela “Pessoa“) e os outros DataSets seriam as tabelas de “Lookup” (no nosso caso as tabelas “Empresa” e “Cargo“). Então, através da função “Lookup“, nós conseguimos pegar dados das tabelas de “Empresa” e “Cargo” no nosso relatório (seria a mesma ideia do PROCV ou VLOOKUP do Excel).
Para vermos como ficaria essa alternativa, vamos criar uma cópia do “RelatorioPessoa“, dando o nome de “RelatorioPessoaMultiplasTabelas“. Dentro desse relatório, vamos excluir o DataSet que tínhamos criado no relatório original (o “DataSetPessoa“) e vamos adicionar três novos DataSets, cada um apontando para uma tabela do nosso DataSetPessoa:
Em seguida, vamos alterar o DataSource da nossa tabela, de forma que ele aponte para o DataSet “Pessoa“:
E agora é que vem o segredo. Como é que conseguimos pegar os dados da Empresa, uma vez que o DataSet da tabela não tem essas informações? Simples, através da expressão Lookup! Por exemplo, para pegarmos o nome fantasia da empresa, a expressão ficaria assim:
O primeiro parâmetro dessa expressão deve ser o nome do campo na tabela de origem (campo “EmpresaID” da tabela “Pessoa“). Já o segundo parâmetro corresponde ao nome do campo na tabela destino (no nosso caso, “EmpresaID“, que é o nome do campo chave na tabela “Empresa“). Ou seja, esses dois primeiros parâmetros indicam os campos de relacionamento entre a tabela origem e a tabela destino.
Em seguida, o terceiro parâmetro deve conter o nome que você quer pegar na tabela de destino. Nesse caso, queremos pegar o campo “NomeFantasia“. Por fim, no último parâmetro nós temos que indicar o nome da tabela de lookup (nesse caso, “Empresa“).
Fácil, não? Veja só como fica a expressão para os outros campos:
' Razão social:
=Lookup(Fields!EmpresaID.Value, Fields!EmpresaID.Value, Fields!RazaoSocial.Value, "Empresa")
' CNPJ
=Lookup(Fields!EmpresaID.Value, Fields!EmpresaID.Value, Fields!CNPJ.Value, "Empresa")
' Nome do Cargo
=Lookup(Fields!CargoID.Value, Fields!CargoID.Value, Fields!Nome.Value, "Cargo")
' Descrição do Cargo
=Lookup(Fields!CargoID.Value, Fields!CargoID.Value, Fields!Descricao.Value, "Cargo")
Agora que já temos o nosso novo relatório, vamos criar um novo formulário para exibi-lo (Form2). Como fizemos no primeiro formulário, nós temos que adicionar um controle exibidor do Report Viewer, só que dessa vez nós selecionaremos o nosso novo relatório (“RelatorioPessoaMultiplasTabelas“). No code-behind, nós não temos mais que criar um DataSetRelatorio, mas sim, temos que passar cada uma das tabelas para o relatório em DataSources diferentes:
' VB.NET
Private Sub Form2_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim DsPessoa = New DataSetPessoa()
DsPessoa.PreencherComDadosDeExemplo()
Me.ReportViewer1.LocalReport.DataSources.Clear()
Me.ReportViewer1.LocalReport.DataSources.Add(New Microsoft.Reporting.WinForms.ReportDataSource("Pessoa", DirectCast(DsPessoa.Pessoa, DataTable)))
Me.ReportViewer1.LocalReport.DataSources.Add(New Microsoft.Reporting.WinForms.ReportDataSource("Empresa", DirectCast(DsPessoa.Empresa, DataTable)))
Me.ReportViewer1.LocalReport.DataSources.Add(New Microsoft.Reporting.WinForms.ReportDataSource("Cargo", DirectCast(DsPessoa.Cargo, DataTable)))
Me.ReportViewer1.RefreshReport()
End Sub
Por fim, vamos alterar o objeto de inicialização para que o nosso Form2 seja exibido (ao invés do Form1). Em projetos C#, fazemos isso no arquivo Program.cs:
Application.Run(new Form2());
Já no VB.NET, temos que alterar o “Startup Form” nas propriedades do projeto:
Pronto! Execute o projeto e veja que o resultado é idêntico ao primeiro relatório:
Concluindo
Quando estamos desenvolvendo relatórios para as nossas aplicações, não é tão difícil nos depararmos com uma situação em que precisamos de dados vindos de múltiplas tabelas. Como o Report Viewer não suporta a exibição de dados de mais de uma tabela no mesmo controle e também não tem o conceito de relacionamentos entre tabelas (como temos no Crystal Reports), a saída mais utilizada é a criação de um segundo DataSet (ou classe) onde todos os dados são consolidados em uma única DataTable.
Apesar dessa alternativa ser a mais utilizada, ela não é a mais ideal. O que pouca gente sabe é que nós podemos utilizar a expressão Lookup para pegarmos dados de outras tabelas no Report Viewer. Esse procedimento é muito mais simples do que termos que criar um DataSet consolidado para cada relatório.
No artigo de hoje você conferiu essas duas alternativas para a exibição de dados vindos de múltiplas tabelas no Report Viewer. Como você pode perceber, o resultado é idêntico, então, não pense duas vezes se você puder utilizar a expressão Lookup nos seus relatórios, que é muito mais prática.
E você, já passou por essa situação onde você tinha que exibir dados de múltiplas tabelas no Report Viewer? Se sim, como é que você resolveu? Você já conhecia a expressão Lookup? Conte-nos mais detalhes na caixa de comentários!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.
Você já desenvolveu um aplicativo que possui referências a diversas bibliotecas? Caso positivo, você sabe a dor de cabeça que enfrentamos na hora de distribuirmos esse tipo de aplicação. Basta esquecer uma simples dll e pronto: nossa aplicação não funcionará corretamente.
O que talvez você não saiba é que nós conseguimos mesclar executável e dlls em um arquivo só através da ferramenta chamada ILMerge. O resultado será um único executável (ou dll), tornando o processo de deployment muito mais fácil.
No artigo de hoje, confira como utilizar o ILMerge via linha de comando e conheça também o ILMergeGui, uma interface visual que interage com o ILMerge e facilita muito a nossa vida quando precisamos mesclar assemblies.
Criando um projeto de exemplo
Para entendermos como juntar vários assemblies em um único executável ou dll, vamos criar um exemplo extremamente simples. Esse exemplo será formado por um projeto “Console Application” (.exe) e dois projetos “Class Library” (.dll). Darei o nome de “ExemploILMerge” para a solução que será criada com a “Console Application“. Dentro dessa solution, vamos adicionar outros dois projetos do tipo “Class Library“, dando o nome de “Biblioteca1” e “Biblioteca2” respectivamente. A estrutura da solução deve ficar parecida com a imagem abaixo:
Dentro da “Class1” da “Biblioteca1“, vamos adicionar um método estático (chamado “Metodo“) que retornará uma string contendo “Biblioteca1.Class1.Metodo“:
// C#
namespace Biblioteca1
{
public class Class1
{
public static string Metodo()
{
return "Biblioteca1.Class1.Metodo";
}
}
}
' VB.NET
Public Class Class1
Public Shared Function Metodo()
Return "Biblioteca1.Class1.Metodo"
End Function
End Class
Faremos exatamente a mesma coisa na “Class1” da “Biblioteca2“, porém, nesse caso o método deverá retornar a string contendo “Biblioteca2.Class1.Metodo“:
// C#
namespace Biblioteca2
{
public class Class1
{
public static string Metodo()
{
return "Biblioteca2.Class1.Metodo";
}
}
}
' VB.NET
Public Class Class1
Public Shared Function Metodo()
Return "Biblioteca2.Class1.Metodo"
End Function
End Class
Em seguida, vamos adicionar as referências para “Biblioteca1” e “Biblioteca2” no nosso projeto “Console Application“. Para isso, temos que clicar com o botão direito em “References” e escolher os projetos que estarão listados dentro da categoria “Projects“:
Por fim, vamos até a classe “Program” (ou “Module1” no VB.NET), onde imprimiremos o resultado das chamadas para os métodos das bibliotecas:
' VB.NET
Sub Main()
Console.WriteLine(Biblioteca1.Class1.Metodo())
Console.WriteLine(Biblioteca2.Class1.Metodo())
Console.ReadLine()
End Sub
Execute a solução e veja o resultado na janela de console:
Nada sofisticado, não é mesmo? Porém, se dermos uma olhada no diretório de saída da solução (onde os projetos são compilados), veremos que temos 3 assemblies (um executável e duas dlls):
Isso quer dizer que a aplicação só funcionará no computador do cliente caso tivermos todos esses arquivos distribuídos na pasta da aplicação. Lembre-se que neste artigo estamos falando de uma solução extremamente simples. Imagine em um cenário real onde diversas bibliotecas são utilizadas nos nossos projetos? A quantidade de assemblies que devem ser distribuídos pode ser bem grande e isso pode se tornar um problema. É por isso que o ILMerge foi desenvolvido: para juntar todos os assemblies em um único executável ou dll!
Baixando e utilizando o ILMerge via linha de comando
O ILMerge pode ser baixado no Download Center da Microsoft ou no formato de um pacote NuGet. Nesta seção do artigo eu vou utilizar a opção do pacote NuGet. Para instalá-lo no seu projeto, abra a janela “Manage NuGet Packages” e procure por “ILMerge“:
Outra opção é digitar o comando “Install-Package ilmerge” diretamente no NuGet Package Manager. Se você tiver alguma dúvida sobre esse processo, confira este artigo onde eu mostro como gerenciar pacotes do NuGet no Visual Studio.
Uma vez instalado, o executável do NuGet estará disponível dentro do subdiretório “packages\ilmerge.2.14.1208\tools” da nossa solução:
Para facilitar a nossa vida, vamos copiar esse executável para a pasta de saída da nossa solução (bin\debug) e, em seguida, vamos abrir um prompt de comando nesse diretório. A propósito, você sabia que dá para abrir um prompt de comando dentro de um diretório específico através do Windows Explorer? Saca só:
Com o prompt de comando aberto, vamos entender a sintaxe básica do ILMerge. Primeiramente, temos que indicar o tipo de saída através do argumento “/target“. Existem três opções para esse argumento: “library“, “exe” ou “winexe“. A opção “library” gerará uma dll, já as opções “exe” e “winexe” gerarão um executável. Mas, qual é a diferença entre essas duas opções que geram executáveis? Simples: a opção “exe” gera um executável do tipo “Console Application” e a opção “winexe” gera um executável do tipo “aplicação Windows” (Windows Forms / WPF). Se você confundir esses dois tipos de executável, a aplicação não funcionará.
Em seguida, com o argumento “/out” nós temos que definir o nome do arquivo de destino. No nosso exemplo, nós utilizaremos o nome “Executavel.exe“. Por fim, temos que passar a lista dos arquivos que deverão ser mesclados (no nosso caso, “ExemploILMerge.exe“, “Biblioteca1.dll” e “Biblioteca2.dll“).
Veja como fica a linha de comando para mesclarmos os arquivos da nossa solução:
Ao executarmos essa linha de comando, teremos o novo executável gerado na pasta bin\debug:
Teste a execução dessa nova aplicação e veja que o comportamento é exatamente o mesmo da aplicação original. Entretanto, agora nós não precisamos mais distribuir três assemblies separados. Basta distribuirmos o arquivo “Executavel.exe” nos computadores clientes e tudo deve funcionar perfeitamente.
Utilizando a ferramenta visual ILMergeGui
Muito legal esse esquema do ILMerge, mas, trabalhar com linha de comando muitas vezes acaba enchendo o saco, não é mesmo? Imagine uma solução onde tenhamos que mesclar 30 assemblies. Vamos ter que digitar o nome dos 30 assemblies na linha de comando? Nada prático.
Foi pensando nisso que uma galera da comunidade resolveu desenvolver uma interface gráfica para o ILMerge. Por trás dos panos o “ILMerge.exe” será chamado passando os argumentos corretos de acordo com o que foi escolhido na interface visual. Essa interface gráfica é chamada ILMergeGui.
Uma vez instalada e executada, pode ser que você receba este erro:
Esse erro acontece quando utilizamos o ILMerge através do pacote NuGet (ao invés de instalarmos pelo Download Center da Microsoft). Nesse caso, o ILMergeGui não consegue encontrar o “ILMerge.exe” e acaba dando esse erro. Para resolvermos esse contratempo, temos três opções:
1) Instalar o ILMerge utilizando o instalador baixado no Download Center 2) Copiar o ILMerge.exe para o diretório C:\Windows\System32 3) Copiar o ILMerge.exe para dentro do diretório de instalação do ILMergeGui
Eu recomendo que você utilize a primeira ou segunda opções. O ILMergeGui é instalado através do ClickOnce, portanto, o seu diretório de instalação fica nas entranhas do sistema de arquivos, o que dificulta um pouco a localização do diretório para utilizarmos a terceira alternativa. No meu computador, por exemplo, o ILMergeGui foi instalado dentro deste diretório: “C:\Users\andrealveslima\AppData\Local\Apps\2.0\TKL4XB63.2CN\TLE58ZPL.D5G\ilme..tion_f70b3bef76080fc4_0002.0000_182dad39646c61d9“:
Enfim, com esse contratempo resolvido, ao abrirmos o ILMergeGui novamente, nós não receberemos mais nenhuma mensagem de erro. A interface do ILMergeGui é muito simples. Na parte de cima (“Assemblies to merge“) nós temos que arrastar os arquivos que devem ser mesclados (no nosso caso ExemploILMerge.exe, Biblioteca1.dll e Biblioteca2.dll) e na parte inferior (“Output assembly“) nós escolhemos o caminho e o nome do executável ou dll onde o resultado deverá ser gerado:
Aí é só clicar em “Merge” e pronto! Bem mais fácil do que trabalhar com o ILMerge via linha de comando, não é mesmo?
Concluindo
A ferramenta ILMerge serve para mesclarmos vários assemblies (.exe e .dll) em um único assembly. Isso pode ser muito útil nas situações em que as nossas aplicações possuam muitas referências a dlls externas.
No artigo de hoje você conferiu como juntar executável e dlls em um arquivo só utilizando o ILMerge. Você viu também que o ILMerge só trabalha com linha de comando, porém, com a ferramenta ILMergeGui, nós temos à nossa disposição uma interface visual que facilita bastante a nossa vida.
E você, já precisou juntar vários assemblies em um só arquivo? Você utilizou o ILMerge ou alguma outra ferramenta? Tudo funcionou certinho ou você enfrentou algum desafio na distribuição dos seus aplicativos? Espero os seus comentários logo abaixo!
Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.