Blazor - Despesas Pessoais com Gráfico de Barras e Pizza - II


Neste artigo vamos iniciar a criação de uma aplicação Blazor Web Assemply para gerenciar despesas pessoais onde vamos criar gráficos de barras e de pizza.

Continuando a primeira parte do artigo vamos implementar o Repositório e os Controladores no projeto Server.

Implementando o padrão Repository na memória

Crie no projeto Server a pasta Repositories e nesta pasta crie a interface IRepository:

using System.Collections.Generic;
namespace Financas.Pessoais.Server.Repositories
{
    public interface IRepository<T>
    {
        void Add(T entity);
        void Remove(T entity);
        IEnumerable<T> GetAll();
    }
}

Observe que definimos o contrato para implementar os métodos para Adicionar, Remover e Listar objetos pois como estamos trabalhando na memória a atualização é automática.

Na mesma pasta vamos criar a classe concreta MemoryRepository<T> que implementa esta interface:

using System.Collections.Generic;
namespace Financas.Pessoais.Server.Repositories
{
    public class MemoryRepository<T> : IRepository<T>
    {
        private readonly IList<T> _entities;
        public MemoryRepository()
        {
            _entities = new List<T>();
        }
        public void Add(T entity)
        {
            _entities.Add(entity);
        }
        public IEnumerable<T> GetAll()
        {
            return _entities;
        }
        public void Remove(T entity)
        {
            _entities.Remove(entity);
        }
    }
}

Temos aqui a implementação do repositório na memória de forma genérica e assim poderemos usar essa implementação para qualquer  tipo T que no nosso exemplo vai representar a classe Receita e Despesa.

Agora vamos usar esta implementação e criar a classe DadosTestes e nesta classe vamos definir dois métodos de extensão AddDespesasRepository e AddReceitasRepository para IServiceCollection onde vamos popular o repositório com dados de receitas e despesas.

using Financas.Pessoais.Shared;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace Financas.Pessoais.Server.Repositories
{
    public static class DadosTestes
    {
        public static void AddReceitasRepository(this IServiceCollection services)
        {
            var hoje = DateTime.Today;
            var ultimoMes = DateTime.Today.AddMonths(-1);
            var mesAnterior = DateTime.Today.AddMonths(-2);
            var receitaRepository = new MemoryRepository<Receita>();

            receitaRepository.Add(new Receita { Data = new DateTime(mesAnterior.Year, mesAnterior.Month, 25), Valor = 2480, Categoria = CategoriaReceita.Salário, Descricao = "Salário mensal" });
            receitaRepository.Add(new Receita { Data = new DateTime(mesAnterior.Year, mesAnterior.Month, 12), Valor = 440, Categoria = CategoriaReceita.Vendas, Descricao = "Vendas" });
            receitaRepository.Add(new Receita { Data = new DateTime(ultimoMes.Year, ultimoMes.Month, 25), Valor = 2480, Categoria = CategoriaReceita.Salário, Descricao = "Salário mensal" });
            receitaRepository.Add(new Receita { Data = new DateTime(ultimoMes.Year, ultimoMes.Month, 12), Valor = 790, Categoria = CategoriaReceita.Vendas, Descricao = "Vendas" });
            receitaRepository.Add(new Receita { Data = new DateTime(ultimoMes.Year, ultimoMes.Month, 4), Valor = 387, Categoria = CategoriaReceita.Lucros, Descricao = "Dividendos" });
            receitaRepository.Add(new Receita { Data = new DateTime(hoje.Year, hoje.Month, 25), Valor = 2480, Categoria = CategoriaReceita.Salário, Descricao = "Salário mensal" });
            receitaRepository.Add(new Receita { Data = new DateTime(hoje.Year, hoje.Month, 14), Valor = 680, Categoria = CategoriaReceita.Vendas, Descricao = "Vendas" });
            receitaRepository.Add(new Receita { Data = new DateTime(hoje.Year, hoje.Month, 12), Valor = 245, Categoria = CategoriaReceita.Vendas, Descricao = "Vendas" });
            services.AddSingleton<IRepository<Receita>>(receitaRepository);
        }

        public static void AddDespesasRepository(this IServiceCollection services)
        {
            var hoje = DateTime.Today;
            var ultimoMes = DateTime.Today.AddMonths(-1);
            var mesAnterior = DateTime.Today.AddMonths(-2);
            var despesaRepository = new MemoryRepository<Despesa>();

            despesaRepository.Add(new Despesa { Data = new DateTime(mesAnterior.Year, mesAnterior.Month, 25), Valor = 480, Categoria = CategoriaDespesa.Alimentação, Descricao = "Compra Mensal" });
            despesaRepository.Add(new Despesa { Data = new DateTime(mesAnterior.Year, mesAnterior.Month, 12), Valor = 40, Categoria = CategoriaDespesa.Transporte, Descricao = "Gasta com gasolina" });
            despesaRepository.Add(new Despesa { Data = new DateTime(ultimoMes.Year, ultimoMes.Month, 25), Valor = 280, Categoria = CategoriaDespesa.Alimentação, Descricao = "Compra semanal" });
            despesaRepository.Add(new Despesa { Data = new DateTime(ultimoMes.Year, ultimoMes.Month, 12), Valor = 10, Categoria = CategoriaDespesa.Extras, Descricao = "Gastos diversos" });
            despesaRepository.Add(new Despesa { Data = new DateTime(ultimoMes.Year, ultimoMes.Month, 4), Valor = 50, Categoria = CategoriaDespesa.Poupança, Descricao = "Poupança pessoal" });
            despesaRepository.Add(new Despesa { Data = new DateTime(hoje.Year, hoje.Month, 25), Valor = 80, Categoria = CategoriaDespesa.Transporte, Descricao = "Gasto com combustível" });
            despesaRepository.Add(new Despesa { Data = new DateTime(hoje.Year, hoje.Month, 14), Valor = 60, Categoria = CategoriaDespesa.Educação, Descricao = "Compra livro" });
            despesaRepository.Add(new Despesa { Data = new DateTime(hoje.Year, hoje.Month, 12), Valor = 25, Categoria = CategoriaDespesa.Vestuário, Descricao = "Compra camiseta" });
            services.AddSingleton<IRepository<Despesa>>(despesaRepository);
        }
    }
}

Com as implementações prontas temos que registrar os serviços na classe Startup:

 public void ConfigureServices(IServiceCollection services)
 {
            services.AddReceitasRepository();
            services.AddDespesasRepository();
            services.AddControllersWithViews();
            services.AddRazorPages();
 }

Temos assim prontos o repositório e os dados para testes e agora vamos criar os controladores ReceitasControllers e DespesasControllers na pasta Controllers :

Criando os controladores

Na pasta Controllers crie o Controller ReceitasController:

using Financas.Pessoais.Server.Repositories;
using Financas.Pessoais.Shared;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Financas.Pessoais.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ReceitasController : ControllerBase
    {
        private readonly IRepository<Receita> _receitaRepository;
        public ReceitasController(IRepository<Receita> receitaRepository)
        {
            _receitaRepository = receitaRepository;
        }

        [HttpGet]
        public IEnumerable<Receita> Get()
        {
            return _receitaRepository.GetAll()
                       .OrderBy(r => r.Data);
        }

        [HttpPost]
        public void Post(Receita receita)
        {
            _receitaRepository.Add(receita);
        }

        [HttpDelete("{id?}")]
        public void Delete(Guid id)
        {
            var entity = _receitaRepository.GetAll()
                  .Single(item => item.Id == id);

            _receitaRepository.Remove(entity);
        }
    }
}

Nesta Web API injetamos a instância do repositório no construtor do controller e implementamos os métodos Get, Post e Delete.

Agora vamos repetir o procedimento para o controller DepesasController:

using Financas.Pessoais.Server.Repositories;
using Financas.Pessoais.Shared;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Financas.Pessoais.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]

    public class DespesasController : ControllerBase
    {
        private readonly IRepository<Despesa> _receitaRepository;
        public DespesasController(IRepository<Despesa> receitaRepository)
        {
            _receitaRepository = receitaRepository;
        }

        [HttpGet]
        public IEnumerable<Despesa> Get()
        {
            return _receitaRepository.GetAll()
                       .OrderBy(r => r.Data);
        }

        [HttpPost]
        public void Post(Despesa receita)
        {
            _receitaRepository.Add(receita);
        }

        [HttpDelete("{id?}")]
        public void Delete(Guid id)
        {
            var entity = _receitaRepository.GetAll()
                  .Single(item => item.Id == id);

            _receitaRepository.Remove(entity);
        }
    }
}

Com isso concluímos as implementações no projeto Server e podemos retornar ao projeto Client para criar os formulários para Despesa e Receita.

Exibindo os dados e concluindo o formulário ReceitaForm

Já temos o formulário ReceitaForm criado e vamos agora incluir o código que vai acessar o API no projeto Server e apresentar os dados de testes para receitas e concluir as funcionalidades do formulário para permitir incluir dados de receitas.

Dessa forma vamos ajustar o código do componente ReceitaForm incluindo a validação dos dados, o envio dos dados acessando a Web API Receitas :

@inject HttpClient Http;

<div class="card">
    <div class="card-header" style="background-color: aqua; font-weight: bold">
        Incluir receita
    </div>
    <div class="card-body">
        <EditForm Model="@receita" OnValidSubmit="@HandleValidSubmit">
            <DataAnnotationsValidator />
            <ValidationSummary />

            <div class="form-group">
                <label for="datainput">Data</label>
                <InputDate class="form-control" id="datainput" @bind-Value="receita.Data" />
            </div>
            <div class="form-group">
                <label for="descricaoinput">Descrição</label>
                <InputText class="form-control" id="descricaoinput" @bind-Value="receita.Descricao" />
            </div>
            <div class="form-group">
                <label for="categoriainput">Categoria</label>
                <InputSelect class="form-control" id="categoriainput" @bind-Value="receita.Categoria">
                    @{
                        foreach (var value in Enum.GetValues(typeof(CategoriaReceita)))
                        {
                            <option value="@value">@value</option>
                        }
                    }
                </InputSelect>
            </div>
            <div class="form-group">
                <label for="valorinput">Valor</label>
                <InputNumber class="form-control" id="valorinput" @bind-Value="receita.Valor" />
            </div>
            <div>
                <button type="submit" class="btn btn-primary">Enviar</button>
            </div>
        </EditForm>
    </div>
</div>

@code{

    private ReceitaDTO receita = new ReceitaDTO { Data = DateTime.Today };

    [Parameter]
    public EventCallback OnSubmitCallback { get; set; }

    public async Task HandleValidSubmit()
    {
        await Http.PostAsJsonAsync<ReceitaDTO>("api/Receitas", receita);
        await OnSubmitCallback.InvokeAsync();
    }
}

Note que injetamos uma instância do serviço HttpClient para usar o método PostAsJsonAsync e acessar a API. Isso é possível pois na classe Program incluímos uma implementação do tipo HttpClient no contêiner DI nativo.

Assim podemos postar os dados do componente ReceitaForm mas a tabela faz parte do componente Pai, Receitas.razor e precisamos informar à pagina Receitas para atualizar os dados sempre que um usuário clicar no botão enviar do componente ReceitaForm.

Por enquanto, vamos usar uma solução simples. Vamos definir uma nova propriedade no componente ReceitaForm. Criamos uma propriedade pública do tipo EventCallback e a nomeamos OnSubmitCallback. Também adicionamos o atributo Parameter, que disponibiliza esta propriedade quando o componente é usado.

Em seguida, executamos o EventCallback depois de postar os dados na API no método HandleValidSubmit.

A ideia é fornecer um retorno de chamada(EventCallBack) para o componente ReceitaForm que será executado depois de enviarmos os dados para a API.

A seguir  teremos que fornecer o retorno de chamada de dentro da página Receitas quando formos ajustar o  seu código. Assim abra o componente Receitas.razor e ajuste o código conforme abaixo:

@page "/receitas"
@inject HttpClient Http;

<div class="row">
    <div class="col-lg-8">
        <div class="card" >
            <div class="card-header" style="background-color: aqua; font-weight: bold ">
                Receitas
            </div>
            <div class="card-body">
                <table class="table table-striped">
                    <thead>
                        <tr>
                            <th>Data</th>
                            <th>Categoria</th>
                            <th>Descricao</th>
                            <th>Valor</th>
                        </tr>
                    </thead>
                    <tbody>
                        @if (receitas == null)
                        {
                            <tr><td><em>Carregando...</em></td></tr>
                        }
                        else
                        {
                            @foreach (var receita in receitas)
                            {
                    <tr>
                        <td>@receita.Data.ToShortDateString()</td>
                        <td>@receita.Categoria</td>
                        <td>@receita.Descricao</td>
                        <td align="right">@receita.Valor.ToString("C2", CultureInfo.CreateSpecificCulture("pt-BR"))</td>
                         <td><button type="button" class="btn btn-danger btn-sm">Excluir</button></td>
                    </tr>
                            }
                        }
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    <div class="col-lg-4">
        <ReceitaForm OnSubmitCallback="@Refresh"></ReceitaForm>
    </div>

</div>
<div>&nbsp;</div>

@code {
    private Receita[] receitas;
    protected async override Task OnInitializedAsync()
    {
        await CarregaDados();
    }

    protected async Task CarregaDados()
    {
        receitas = await Http.GetFromJsonAsync<Receita[]>("api/Receitas");
        StateHasChanged();
    }

    public async Task Refresh()
    {
        await CarregaDados();
    }   

}

Observe que implementamos um método Refresh. Neste método, queremos recarregar os dados. Já temos esse comportamento no método OnInitializedAsync. Vamos extrair o código em um método CarregaDados  para chamá-lo tanto no método OnInitializedAsync quanto no método Refresh.

Em seguida, queremos fornecer o método Refresh para o componente ReceitaForm. Podemos fazer isso configurando o atributo OnSubmitCallback para o método Refresh na definição do modelo. Não se esqueça de adicionar um símbolo @ na frente do nome do método.

 Agora executando o projeto teremos a exibição dos dados e poderemos incluir novas receitas usando o formulário:

Na  próxima parte do artigo vamos exibir as despesas, criar o formulário para Despesas e implementar a exclusão de uma receita e/ou despesa.

"Voz do que clama no deserto: Preparai o caminho do Senhor; endireitai no ermo vereda a nosso Deus.
Todo o vale será exaltado, e todo o monte e todo o outeiro será abatido; e o que é torcido se endireitará, e o que é áspero se aplainará."
Isaías 40:3,4

Referências:


José Carlos Macoratti