Como criamos relatórios dinâmicos no SSRS 2014


Nós já conversamos sobre como ajudamos uma empresa de fabricação de transformar os processos de treinamento corporativo e desenvolvimento de pessoal. Os funcionários do cliente, que estavam se afogando em documentos em papel e planilhas do Excel, receberam um aplicativo conveniente para iPad e um portal da web. Uma das funções mais importantes deste produto é a criação de relatórios dinâmicos pelos quais os gerentes julgam o trabalho dos funcionários “no campo”. São documentos enormes, com dezenas de campos e tamanhos médios de 3000 * 1600 pixels.

Neste artigo, falaremos sobre como implantar essa beleza com base no Microsoft SQL Server Reporting Services, por que esse back-end pode ser um mau amigo do portal da Web e quais truques ajudarão a estabelecer seu relacionamento. Toda a parte comercial da solução já foi descrita no artigo anterior; portanto, aqui nos concentramos em questões técnicas. Vamos começar!


Formulação do problema


Temos um portal com o qual várias centenas de usuários trabalham. Eles são organizados em uma hierarquia gradual, onde cada usuário tem um supervisor de uma classificação superior. Essa diferenciação de direitos é necessária para que os usuários possam criar eventos com qualquer funcionário subordinado. Você pode pular etapas, ou seja, o usuário pode iniciar a atividade com um funcionário de qualquer nível inferior a ele.

Que eventos são significados aqui? Isso pode ser treinamento, suporte ou certificação de um funcionário de uma empresa comercial, que o supervisor conduz no ponto de venda. O resultado desse evento é um questionário preenchido em um iPad com classificações de funcionários para qualidades e habilidades profissionais.

De acordo com os dados do questionário, você pode preparar estatísticas, por exemplo:

  • Quantos eventos com seus subordinados desse tipo Vasya Ivanov criou em um mês? Quantos deles foram concluídos?
  • Qual é a porcentagem de classificações satisfatórias? Quais perguntas os comerciantes respondem pior? Qual gerente é pior em fazer testes?

Essas estatísticas estão contidas nos relatórios que podem ser criados via interface da web, nos formatos XLS, PDF, DOCX e impressos. Todas essas funções são projetadas para gerentes em diferentes níveis.

O conteúdo e o design dos relatórios são definidos nos modelos , permitindo definir os parâmetros necessários. Se, no futuro, os usuários precisarem de novos tipos de relatórios, o sistema poderá criar modelos, especificar parâmetros modificáveis ​​e adicionar um modelo ao portal. Tudo isso - sem interferir no código fonte e nos processos de trabalho do produto.

Especificações e limitações


O portal é executado na arquitetura de microsserviço, a frente está escrita em Angular 5. O recurso usa autorização JWT, suporta navegadores Google Chrome, Firefox, Microsoft Edge e IE 11 .

Todos os dados são armazenados no MS SQL Server 2014. O SQL Server Reporting Services (SSRS) está instalado no servidor, o cliente usa e não vai recusar. Daí a limitação mais importante: o acesso ao SSRS é fechado a partir do exterior, para que você possa acessar a interface da web e o SOAP somente da rede local através da autorização NTLM.

Algumas palavras sobre o SSRS
SSRS – , , . docs.microsoft.com, SSRS (API) ( Report Server, - HTTP).

Atenção, a pergunta: como concluir a tarefa sem métodos manuais, com recursos mínimos e benefícios máximos para o cliente?

Como o cliente possui o SSRS em um servidor dedicado, deixe o SSRS fazer todo o trabalho sujo de gerar e exportar relatórios. Então não precisamos escrever nosso próprio serviço de relatórios, exportar módulos para XLS, PDF, DOCX, HTML e a API correspondente.

Portanto, a tarefa era fazer o SSRS fazer amizade com o portal e garantir a operação das funções especificadas na tarefa. Então, vamos examinar a lista desses cenários - sutilezas interessantes foram encontradas em quase todos os pontos.

Estrutura da solução


Como já temos o SSRS, existem todas as ferramentas para gerenciar modelos de relatório:

  • Servidor de Relatório - responsável por toda a lógica de trabalhar com relatórios, seu armazenamento, geração, gerenciamento e muito mais.
  • Gerenciador de relatórios - um serviço com uma interface da web para gerenciar relatórios. Aqui você pode carregar modelos criados no SQL Server Data Tools para o servidor, configurar direitos de acesso, fontes de dados e parâmetros (incluindo aqueles que podem ser alterados ao relatar solicitações). Ele é capaz de gerar relatórios sobre modelos baixados e enviá-los para vários formatos, incluindo XLS, PDF, DOCX e HTML.

Total: criamos modelos no SQL Server Data Tools, com a ajuda do Report Manager, os preenchemos no Report Server, configuramos - e está pronto. Podemos gerar relatórios, alterar seus parâmetros.

A próxima pergunta: como solicitar a geração de relatórios sobre modelos específicos por meio do portal e obter o resultado inicial para saída na interface do usuário ou fazer o download no formato desejado?

Relatórios do SSRS para o portal


Como dissemos acima, o SSRS possui sua própria API para acessar relatórios. Mas não queremos distribuir suas funções por razões de segurança e higiene digital - precisamos apenas solicitar dados do SSRS na forma correta e transmitir o resultado ao usuário. O gerenciamento de relatórios será tratado por uma equipe de clientes especialmente treinada.

Como o acesso ao SSRS é apenas da rede local, a troca de dados entre o servidor e o portal é feita por meio de um serviço proxy.


Troca de dados entre o portal e o servidor

Vamos ver como funciona e por que o ReportProxy está aqui.

Portanto, no lado do portal, temos um ReportService, que o portal acessa para relatórios. O serviço verifica a autorização do usuário, o nível de seus direitos, converte dados do SSRS na forma desejada no contrato.

A API ReportService contém apenas 2 métodos, que são suficientes para nós:

  1. GetReports - fornece os identificadores e nomes de todos os modelos que o usuário atual pode receber;
  2. GetReportData (formato, parâmetros) - fornece dados de relatório exportados e prontos no formato especificado, com um determinado conjunto de parâmetros.

Agora você precisa desses 2 métodos para poder se comunicar com o SSRS e obter os dados necessários da forma correta. A partir da documentação, sabe-se que podemos acessar o servidor de relatório via HTTP usando a API SOAP. Parece que o quebra-cabeça está se desenvolvendo ... Mas, de fato, uma surpresa nos espera aqui.

Como o SSRS está fechado para o mundo externo e você pode alcançá-lo somente através da autenticação NTLM, ele não está disponível diretamente no portal SOAP. Há também nossos próprios desejos:

  • Conceda acesso apenas ao conjunto de funções necessário e até proíba a alteração;
  • Se você precisar mudar para outro sistema de relatórios, as edições no ReportService deverão ser mínimas e, melhor, não serão necessárias.

É aqui que o ReportProxy nos ajuda, localizado na mesma máquina que o SSRS e responsável por solicitações de proxy do ReportService para o SSRS. O processamento de solicitações é o seguinte:

  1. o serviço recebe uma solicitação do ReportService, verifica a autorização da JWT;
  2. de acordo com o método API, o proxy passa pelo protocolo SOAP no SSRS para obter os dados necessários, efetuando login no NTLM ao longo do caminho;
  3. Os dados recebidos do SSRS são enviados de volta ao ReportService em resposta à solicitação.

De fato, o ReportProxy é um adaptador entre o SSRS e o ReportService.
O controlador é o seguinte:
[BasicAuthentication]
public class ReportProxyController : ApiController
{
    [HttpGet()]
    public List<ReportItem> Get(string rootPath)
    {
        //  ...
    }

    public HttpResponseMessage Post([FromBody]ReportRequest request)
    {
        //  ...
    }
}

BasicAuthentication :

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        var authHeader = actionContext.Request.Headers.Authorization;

        if (authHeader != null)
        {
            var authenticationToken = actionContext.Request.Headers.Authorization.Parameter;
            var tokenFromBase64 = Convert.FromBase64String(authenticationToken);
            var decodedAuthenticationToken = Encoding.UTF8.GetString(tokenFromBase64);
            var usernamePasswordArray = decodedAuthenticationToken.Split(':');
            var userName = usernamePasswordArray[0];
            var password = usernamePasswordArray[1];

            var isValid = userName == BasiAuthConf.Login && password == BasiAuthConf.Password;

            if (isValid)
            {
                var principal = new GenericPrincipal(new GenericIdentity(userName), null);
                Thread.CurrentPrincipal = principal;

                return;
            }
        }

        HandleUnathorized(actionContext);
    }

    private static void HandleUnathorized(HttpActionContext actionContext)
    {
        actionContext.Response = actionContext.Request.CreateResponse(
            HttpStatusCode.Unauthorized
        );

        actionContext.Response.Headers.Add(
            "WWW-Authenticate", "Basic Scheme='Data' location = 'http://localhost:"
        );
    }
}


Como resultado, o processo fica assim:

  1. A frente envia uma solicitação http para ReportService;
  2. ReportService envia uma solicitação http para ReportProxy;
  3. O ReportProxy através da interface SOAP recebe dados do SSRS e envia o resultado ao ReportService;
  4. O ReportService traz o resultado de acordo com o contrato e o entrega ao cliente.

Temos um sistema operacional que solicita uma lista de modelos disponíveis, acessa o SSRS para relatórios e os apresenta à frente em todos os formatos suportados. Agora você precisa exibir os relatórios gerados na frente, de acordo com os parâmetros especificados, dar a oportunidade de enviá-los para arquivos XLS, PDF, DOCX e imprimir. Vamos começar com a exibição.

Trabalhando com relatórios do SSRS no portal


À primeira vista, é uma questão cotidiana - o relatório é fornecido em formato HTML, para que possamos fazer o que quisermos com ele! Vamos incorporá-lo à página, pintá-lo com estilos de design, e a coisa está no chapéu. De fato, houve armadilhas suficientes.

De acordo com o conceito de design, a seção de relatórios no portal deve consistir em duas páginas:

1) uma lista de modelos onde podemos:

  • Visualizar estatísticas de atividades para todo o portal;
  • veja todos os modelos disponíveis para nós;
  • clique no modelo desejado e vá para o gerador de relatórios correspondente.



2) um gerador de relatórios que nos permite:

  • defina os parâmetros do modelo e crie um relatório sobre eles;
  • veja o que aconteceu como resultado;
  • selecione o formato do arquivo de saída, faça o download;
  • imprima o relatório de forma conveniente e visual.



Não houve problemas especiais com a primeira página, por isso não vamos considerar mais. E o gerador de relatórios nos forçou a ligar o engenheiro, para que fosse conveniente que pessoas reais usassem todas as funções no TK.

Problema número 1. Mesas gigantes


De acordo com o conceito de design, esta página deve ter uma área de visualização para que o usuário possa ver seu relatório antes de exportar. Se o relatório não couber na janela, você pode rolar horizontal e verticalmente. Ao mesmo tempo, um relatório típico pode atingir tamanhos de várias telas, o que significa que precisamos colar blocos com os nomes de linhas e colunas. Sem isso, os usuários terão que retornar constantemente ao topo da tabela para lembrar o que significa uma célula específica. Ou, em geral, será mais fácil imprimir um relatório e manter constantemente as folhas necessárias diante dos seus olhos, mas a tabela na tela simplesmente perde seu significado.

Em geral, os blocos colantes não podem ser evitados. E o SSRS 2014 não sabe como corrigir linhas e colunas em um documento MHTML - apenas em sua própria interface da web.

Aqui, lembramos que os navegadores modernos suportam a propriedade adesiva CSS , que apenas fornece a função que precisamos. Colocamos a posição: pegajosa no bloco marcado, especifica o recuo à esquerda ou na parte superior (propriedades esquerda, superior) e o bloco permanecerá no lugar durante a rolagem horizontal e vertical.

Você precisa encontrar um parâmetro que o CSS possa capturar. Valores de célula personalizados que permitem que o SSRS 2014 os capture na interface da Web são perdidos ao exportar para HTML. OK, vamos marcá-los nós mesmos - só entenderíamos como.

Depois de várias horas lendo a documentação e as discussões com os colegas, parecia que não havia opções. E aqui, de acordo com todas as leis da plotagem, o campo Dica de ferramenta apareceu para nós, o que nos permite especificar dicas de ferramentas para células. Descobriu-se que ele é lançado no código HTML exportado no atributo dica de ferramenta - exatamente na marca que pertence à célula personalizada no SQL Server Data Tools. Não houve escolha - não encontramos outra maneira de marcar as células para fixação.

Portanto, você precisa criar regras de marcação e encaminhar marcadores em HTML via ToolTip. Em seguida, usando JS, alteramos o atributo dica de ferramenta para a classe CSS no marcador especificado.

Existem apenas duas maneiras de corrigir células: verticalmente (coluna fixa) e horizontalmente (linha fixa). Faz sentido colocar outro marcador nas células dos cantos, que permanecem no lugar ao rolar nas duas direções - fixa-ambas.

O próximo passo é fazer a interface do usuário. Ao receber um documento HTML, você precisa encontrar todos os elementos HTML com marcadores, reconhecer os valores, definir a classe CSS apropriada e remover o atributo dica de ferramenta para que ele não saia quando você passa o mouse sobre o mouse. Note-se que a marcação resultante consiste em tabelas aninhadas (tags de tabela).

Ver código
type FixationType = 'row' | 'column' | 'both';

init(reportHTML: HTMLElement) {
    //    

    // -  
    const rowsFixed: NodeList = reportHTML.querySelectorAll('[title^="RowFixed"]');
    // -  
    const columnFixed: NodeList = reportHTML.querySelectorAll('[title^="ColumnFixed"]');
    // -    
    const bothFixed: NodeList = reportHTML.querySelectorAll('[title^="BothFixed"]');

    this.prepare(rowsFixed, 'row');
    this.prepare(columnFixed, 'column');
    this.prepare(bothFixed, 'both');
}

//    
prepare(nodeList: NodeList, fixingType: FixationType) {
    for (let i = 0; i < nodeList.length; i++) {
        const element: HTMLElement = nodeList[i];
        //   -
        element.classList.add(fixingType + '-fixed');

        element.removeAttribute('title');
        element.removeAttribute('alt'); //   SSRS

        element.parentElement.classList.add(fixingType  + '-fixed-parent');

        //     ,     
        element.style.width = element.getBoundingClientRect().width  + 'px';
        //     ,     
        element.style.height = element.getBoundingClientRect().height  + 'px';

        //  
        this.calculateCellCascadeParams(element, fixingType);
    }
}


E aqui está um novo problema: com o comportamento em cascata, quando vários blocos que se movem em uma direção são fixados ao mesmo tempo na tabela, as células que vão uma após a outra são colocadas em camadas. Ao mesmo tempo, não está claro quanto cada próximo bloco deve recuar - os recuos terão que ser calculados via JavaScript com base na altura do bloco à sua frente. Tudo isso se aplica às âncoras verticais e horizontais.

O script de correção resolveu o problema.
//      
calculateCellCascadeParams(cell: HTMLElement, fixationType: FixationType) {
    const currentTD: HTMLTableCellElement = cell.parentElement;
    const currentCellIndex = currentTD.cellIndex;

    //   
    currentTD.style.left = '';
    currentTD.style.top = '';

    const currentTDStyles = getComputedStyle(currentTD);

    //  
    if (fixationType === 'row' || fixationType === 'both') {
        const parentRow: HTMLTableRowElement = currentTD.parentElement;

        //        
        //    .
        //   ,    .
        let previousRow: HTMLTableRowElement = parentRow;
        let topOffset = 0;

        while (previousRow = previousRow.previousElementSibling) {
            let previousCellIndex = 0;
            let cellIndexBulk = 0;

            for (let i = 0; i < previousRow.cells.length; i++) {
                if (previousRow.cells[i].colSpan > 1) {
                    cellIndexBulk += previousRow.cells[i].colSpan;
                } else {
                    cellIndexBulk += 1;
                }

                if ((cellIndexBulk - 1) >= currentCellIndex) {
                    previousCellIndex = i;
                    break;
                }
            }

            const previousCell = previousRow.cells[previousCellIndex];

            if (previousCell.classList.contains(fixationType + '_fixed_parent')) {
                topOffset += previousCell.getBoundingClientRect().height;
            }
        }

        if (topOffset > 0) {
            if (currentTDStyles.top) {
                topOffset += <any>currentTDStyles.top.replace('px', '') - 0;
            }

            currentTD.style.top = topOffset + 'px';
        }
    }

    //  
    if (fixationType === 'column' || fixationType === 'both') {
        //       
        //     .
        //   ,    .
        let previousCell: HTMLTableCellElement = currentTD;
        let leftOffset = 0;

        while (previousCell = previousCell.previousElementSibling) {
            if (previousCell.classList.contains(fixationType + '_fixed_parent')) {
                leftOffset += previousCell.getBoundingClientRect().width;
            }
        }

        if (leftOffset > 0) {
            if (currentTDStyles.left) {
                leftOffset += <any>currentTDStyles.left.replace('px', '') - 0;
            }

            currentTD.style.left = leftOffset + 'px';
        }
    }
}


O código verifica as tags dos elementos marcados e adiciona os parâmetros das células fixas ao valor do recuo. No caso de linhas aderentes, sua altura é somada, para colunas, sua largura.


Um exemplo de relatório com uma linha superior fixa,

como resultado, o processo é semelhante a este:

  1. Nós obtemos a marcação do SSRS e colamos no lugar certo no DOM;
  2. Reconhecer marcadores;
  3. Ajuste os parâmetros para o comportamento em cascata.

Como o comportamento de aderência é totalmente implementado por meio do CSS, e o JS está envolvido apenas na preparação do documento recebido, a solução funciona com rapidez suficiente e sem atrasos.

Infelizmente, para o IE, os blocos aderentes tiveram que ser desativados porque não suporta a posição: propriedade pegajosa. O restante - Safari, Mozilla Firefox e Chrome - faz um excelente trabalho.

Ir em frente.

Problema número 2. Exportação de relatórios


Para retirar um relatório do sistema, você deve (1) acessar o SSRS via ReportService para um objeto Blob, (2) obter um link para o objeto através da interface usando o método window.URL.createObjectURL, (3) colocar o link na tag e simular um clique para upload de arquivo.

Isso funciona no Firefox, Safari e em todas as versões do Chrome, exceto a Apple. Para que o IE, Edge e Chrome para iOS também suportassem a função, eu tive que jogar meu cérebro de volta.

No IE e Edge, o evento simplesmente não acionará uma solicitação do navegador para baixar o arquivo. Esses navegadores possuem um recurso que, para simular um clique, é necessária a confirmação do usuário para fazer o download, além de uma indicação clara de outras ações. A solução foi encontrada no método window.navigator.msSaveOrOpenBlob (), disponível no IE e no Edge. Ele apenas sabe como pedir permissão ao usuário para a operação e esclarecer o que fazer em seguida. Portanto, determinamos se o método window.navigator.msSaveOrOpenBlob existe e atuamos na situação.

O Chrome no iOS não teve esse tipo de hack e, em vez de um relatório, temos apenas uma página em branco. Vagando pela Web, encontramos uma história semelhante, a julgar pela qual no iOS 13 esse bug deveria ter sido corrigido. Infelizmente, escrevemos o aplicativo nos dias do iOS 12, por isso decidimos não perder mais tempo e simplesmente desligamos o botão no Chrome para iOS.
Agora, sobre como é o processo final de exportação para a interface do usuário. Há um botão no componente de relatório Angular que inicia uma cadeia de etapas:

  • através dos parâmetros do evento, o manipulador recebe o identificador do formato de exportação (por exemplo, “PDF”);
  • Envia uma solicitação ao ReportService para receber um objeto Blob para o formato especificado;
  • verifica se o navegador é IE ou Edge;
  • quando a resposta vier do ReportService:
    • se for IE ou Edge, chama window.navigator.msSaveOrOpenBlob (fileStream, fileName);
    • caso contrário, ele chamará o método this.exportDownload (fileStream, fileName), em que fileStream é o Blob obtido da consulta ao ReportService e fileName é o nome do arquivo a ser salvo. O método cria uma marca oculta com um link para window.URL.createObjectURL (fileStream), simula um clique e remove a marca.

Com isso resolvido, a última aventura permaneceu.

Problema número 3. Imprimir


Agora podemos ver o relatório no portal e exportá-lo para os formatos XLS, PDF e DOCX. Resta implementar a impressão do documento para obter um relatório preciso de várias páginas. Se a tabela acabou sendo dividida em páginas, cada uma delas deve conter cabeçalhos - os mesmos blocos que mencionamos na seção anterior.

A opção mais fácil é pegar a página atual com o relatório exibido, ocultar tudo supérfluo usando CSS e enviá-la para impressão usando o método window.print (). Este método não funciona imediatamente por vários motivos:

  1. Área de visualização não padrão - o próprio relatório está contido em uma área de rolagem separada, para que a página não se estenda a dimensões horizontais incríveis. Usar window.print () corta o conteúdo que não se ajusta à tela;
  2. , ;
  3. , .

Tudo isso pode ser corrigido usando JS e CSS, mas decidimos economizar tempo para os desenvolvedores e procurar uma alternativa para window.print ().

O SSRS pode nos fornecer imediatamente um PDF pronto com uma paginação apresentável. Isso nos salva de todas as dificuldades da versão anterior, a única pergunta é: podemos imprimir o PDF através de um navegador?

Como o PDF é um padrão de terceiros, os navegadores o suportam por meio de vários plugins do visualizador. Sem plug-in - sem desenhos, então, novamente, precisamos de uma opção alternativa.

E se você colocar o PDF na página como uma imagem e enviar esta página para impressão? Já existem bibliotecas e componentes para o Angular que fornecem essa renderização. Pesquisado, experimentado, implementado.

Para não lidar com os dados que não queremos imprimir, foi decidido transferir o conteúdo renderizado para uma nova página e já executar window.print (). Como resultado, todo o processo é o seguinte:

  1. Solicite o ReportService para exportar o relatório em formato PDF;
  2. Obtemos o objeto Blob, convertemos para um URL (URL.createObjectURL (fileStream)), fornecemos o URL para o visualizador de PDF para renderização;
  3. Tiramos imagens do visualizador de PDF;
  4. Abra uma nova página e adicione uma pequena marcação lá (título, um pequeno recuo);
  5. Adicione a imagem do visualizador de PDF à marcação, chame window.print ().

Após várias verificações, também apareceu na página um código JS que, antes da impressão, verifica se todas as imagens foram carregadas.

Assim, toda a aparência do documento é determinada pelos parâmetros do modelo SSRS, e a interface do usuário não interfere nesse processo. Isso reduz o número de possíveis erros. Como as imagens estão sendo transferidas para impressão, estamos seguros contra qualquer dano ou deformação do layout.

Há também desvantagens:

  • um relatório grande pesará muito, o que afetará adversamente as plataformas móveis;
  • o design não é atualizado automaticamente - cores, fontes e outros elementos do design precisam ser instalados no nível do modelo.

No nosso caso, a adição frequente de novos modelos não era esperada, portanto a solução era aceitável. O desempenho móvel foi tomado como garantido.

A última palavra


Assim, um projeto regular mais uma vez nos faz procurar soluções simples para tarefas não triviais. O produto final atende totalmente aos requisitos de design e fica bonito. E o mais importante, embora não precisássemos procurar os métodos de implementação mais óbvios, a tarefa foi concluída mais rapidamente do que se adotássemos o módulo de relatório original com todas as consequências. E, no final, conseguimos nos concentrar nos objetivos de negócios do projeto.

Source: https://habr.com/ru/post/undefined/


All Articles