quarta-feira, 20 de julho de 2011

ReportViewer: Outra Razão Para “Sys.ArgumentNullException: Value cannot be null. Parameter name: panelsCreated[…]”

ponte-que-partiu-1 
Ponte Que Partiu

CARVALHO. PONTE QUE PARTIU!!! Perdemos uma manhã inteira caçando o erro “Sys.ArgumentNullException: Value cannot be null. Parameter name: panelsCreated[1]” que era gerado em um web form que tinha um controle ReportViewer pra mostrar relatórios na nossa aplicação. A nossa “preguiça” me parece ser um jeito bem fácil de este erro aparecer: copiar um relatório já existente para fazer um novo. Nós criamos uma cópia de um relatório já existente pra reaproveitar o trabalho de layout já feito para o relatório original. Só que a página com o relatório original funcionava ok, mas na página com o novo relatório, o controle ReportViewer mostrava um painel em branco, e o IE mostrava o seguinte erro de JavaScript:

sheila-carvalho
Sheila Carvalho

Sys.ArgumentNullException: Value cannot be null. Parameter name: panelsCreated[1]

O problema foi gerado porque o relatório original tinha 4 parâmetros, e no segunda relatório só usamos 3. O quarto parâmetro não era usado, e não tinha um valor default. E, conforme descobrimos, parâmetros para os quais não são fornecidos um valor provocam o erro acima na renderização do ReportViewer. Bastou retirar a definição do parâmetro no arquivo RDLC e o relatório foi renderizado corretamente.

segunda-feira, 4 de julho de 2011

Linq, Distinct(), IEqualityComparer e GetHashCode()

Disclaimer: esse artigo é sobre um detalhe tão específico do Framework, que vale mais pelas fotos do que pelo conteúdo. Mas se você for uma das 19 pessoas no mundo que querem usar Distinct() em Linq para evitar instâncias repetidas no seu resultado, além das belas paisagens o código pode te ajudar.
GB no Big BenEstou eu encalhado aqui no Galeão, voltando de uma semana nas Európias – eu e Minha Dona (letra maiúscula que não sou doido) fomos conhecer Paris e Londres. Nosso vôo pra Brasília só sai daqui a 6 horas; eu não tô fazendo nada mesmo, então pensei em escrever um pouco sobre Linq, Distinct(), IEqualityComparer e GetHashCode().
Uma aplicação nossa lê de um arquivo um monte de registros de produtos pra dentro de uma List<Produtos>. Aí eu fui jogar estes produtos na tabela de Produtos no BD, mas como havia registros repetidos no arquivo, apareceram vários registros para o mesmo produto no banco.
“Bem, é só fazer o DISTINCT na lista”. Ok. Fui olhar o método Distinct() do Linq e achei duas assinaturas: uma recebe uma expressão lambda cujo resultado, ao ser avaliado para cada registro da lista, define os registros distintos; e outra recebe uma instância de IEqualityComparer, a qual nem dei uma segunda olhada.
Pra gerar minha lista de produtos distintos, escrevi:
// 1ª tentativa: Distinct(p => p) pra retornar uma lista de produtos sem repetidos 
List<Produto> listaSemRepetidos = listaProdutos.Distinct(p => p);
Vênus de Milo no Louvre - Grandes MiércolesEstranhamente, isto fez com que *todos* os objetos em listaProdutos aparececem em listaSemRepetidos. Pensando melhor, faz sentido. Distinct() tem que ver se os objetos são iguais para decidir se eles são repetidos, então deve chamar Equals() – qualquer classe tem Equals(), que é herdado de System.Object - e na implementação default de Equals() dois objetos são iguais se apontam para a mesma instância, e não se seus campos contem o mesmo valor.
Antes de ir escovar bit e partir pra uma implementação customizada de Equals() na minha classe Produtos, resolvi dar uma olhada com mais calma na segunda assinatura do Distinct(), aquela que recebe uma instância de IEqualityComparer. Esta interface contem dois métodos: bool Equals(<T>, <T>) e int GetHashCode(). “Morreu”, pensei eu. “É só implementar a interface e na implementação do Equals(<T>,<T>), comparar os campos das instâncias recebidas”. Minha implementação ficou assim:
public class ComparadorProdutos : IEqualityComparer<AcessoDados.Entidades.Produto>
{ 
    public bool Equals(AcessoDados.Entidades.Produto p1, AcessoDados.Entidades.Produto p2) 
    {
        // Trata os casos nos quais há objetos nulos 
        if(p1 == null && p2 == null) return true;
        if(p1 == null && p2 != null) return false;
        if(p1 != null && p2 == null) return false;
        // Se nenhum dos dois é nulo, retorna true se os campos “importantes” contém o mesmo valor
        return 
        (
            p1.CodigoProduto == p2.CodigoProduto &&
            p1.DataInicial == p2.DataInicial &&
            p1.DataFinal == p2.DataFinal &&
            p1.CodigoNCM == p2.CodigoNCM &&
            p1.Descricao == p2.Descricao &&
            p1.UnidadeMedida == p2.UnidadeMedida &&  
            p1.AliquotaICMS == p2.AliquotaICMS && 
            p1.AliquotaIPI == p2.AliquotaIPI &&
            p1.ReducaoBaseCalculoICMS == p2.ReducaoBaseCalculoICMS &&
            p1.BaseCalculoICMSSubstTrib == p2.BaseCalculoICMSSubstTrib &&
            p1.IdCliente == p2.IdCliente  
        );
    }

    // Retorna qualquer coisa em GetHashCode() pq não vou usar esse trem mesmo
    public int GetHashCode(AcessoDados.Entidades.Produto p)
    {
        if(p == null) return -1;
        else return p.GetHashCode();
    }
}
Na implementação do GetHashCode() coloquei qualquer leseira, porque o que o Distinct() ia usar mesmo era o Equals(). Testei com um programinha console, tudo ok, agora é só chamar o Distinct() passando uma instância de ComparadorProdutos.
// 2ª tentativa: Passando uma implementação de IEqualityComparer para Distinct() 
List<Produto> listaSemRepetidos = listaProdutos.Distinct(new ComparadorProdutos());
Torre de Londres - Show!!!E de novo voltaram *todos* os registros. Caraca. Testei de um lado, do outro, volta pra aplicaçãozinha console pra ver se o Equals() não tem alguma falha, e tudo ok. Só não funciona. Finalmente entreguei os pontos e fui consultar A Fonte De Toda A Sabedoria. E realmente encontrei um artigo “LINQ: Distinct() does not work as expected”, de outro sofredor que passou pela mesma novela. Mas antes de recorrer ao Google, ele colocou um breakpoint em cada um dos métodos da sua implementação de IEqualityComparer e viu que, em vez de chamar Equals(), o método Distinct() chama GetHashCode() para verificar se os objetos recebidos são iguais!!! Se os objetos tem o mesmo valor para GetHashCode() então eles são considerados iguais, e é assim que o Distinct() separa objetos "repetidos" e "únicos". Ele até cita exemplos de boas implementações para o GetHashCode(), mas a melhor dica quanto a isto é que o próprio .NET Framework já fornece uma boa implementação: a usada para tipos anônimos. Então na classe ComparadorProdutos mudei a minha implementação do GetHashCode() para retornar o hash code de um tipo anônimo com os campos da classe Produto que eu queria comparar:
    public int GetHashCode(AcessoDados.Entidades.Produto p) 
    {
        if(p == null) 
            return 0;
        else 
            return new 
            {
                p.CodigoProduto,
                p.DataInicial,
                p.DataFinal,
                p.CodigoNCM,
                p.Descricao,
                p.UnidadeMedida,
                p.AliquotaICMS,
                p.AliquotaIPI,
                p.ReducaoBaseCalculoICMS,
                p.BaseCalculoICMSSubstTrib,
                p.IdCliente 
            }.GetHashCode();
    }
Se duas instâncias da classe Produto tem os mesmos valores nestes campos, seus hash codes serão iguais. E aí meu Distinct(new ComparadorProdutos) finalmente funcionou.

Cris me ajudando na redação do post no Galeão
Cris me ajudando na redação do post no Galeão

É, nada como 6 horas de aeroporto pra gastar…
[]s,
GB